diff --git a/LUA_DEV.md b/LUA_DEV.md new file mode 100644 index 00000000..d89b4a72 --- /dev/null +++ b/LUA_DEV.md @@ -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 = { + [""] = { + 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. diff --git a/app.go b/app.go index 1ee75766..7af31aee 100644 --- a/app.go +++ b/app.go @@ -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) @@ -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) @@ -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 "$", "!": diff --git a/complete.go b/complete.go index dd404362..6f6f5038 100644 --- a/complete.go +++ b/complete.go @@ -49,6 +49,7 @@ var ( "jump-prev", "load", "low", + "luapreviewer-priority", "mark-load", "mark-remove", "mark-save", @@ -57,6 +58,7 @@ var ( "page-down", "page-up", "paste", + "plugin-reload", "push", "quit", "read", @@ -408,7 +410,9 @@ func completeCmd(s string) (matches []compMatch, longest string) { case "sizeunits": matches, longest = matchWord(f[2], []string{"binary", "decimal"}) case "sortby": - matches, longest = matchWord(f[2], []string{"atime", "btime", "ctime", "custom", "ext", "name", "natural", "size", "time"}) + candidates := []string{"atime", "btime", "ctime", "custom", "ext", "name", "natural", "size", "time"} + candidates = append(candidates, getLuaSortingMethodNames()...) + matches, longest = matchWord(f[2], candidates) case "terminalcursor": matches, longest = matchWord(f[2], []string{"default", "block", "underline", "bar", "blinkblock", "blinkunderline", "blinkbar"}) default: @@ -453,8 +457,17 @@ func completeCmd(s string) (matches []compMatch, longest string) { } case "toggle": matches, longest = matchCmdFile(f[len(f)-1], false) + case "luapreviewer-priority": + if len(f)%2 == 0 { + names := getLuaPreviewerNames() + slices.Sort(names) + matches, longest = matchWord(longest, slices.Compact(names)) + } default: - if !slices.Contains(gCmdWords, f[0]) { + expr := gOpts.cmds[f[0]] + if luaExpr, ok := expr.(*luaMsgExpr); ok { + matches, longest = callLuaCommandCompletion(luaExpr, f, longest) + } else if !slices.Contains(gCmdWords, f[0]) { matches, longest = matchCmdFile(f[len(f)-1], false) } } diff --git a/copy.go b/copy.go index bc6f9218..9fb3af5a 100644 --- a/copy.go +++ b/copy.go @@ -6,8 +6,6 @@ import ( "os" "path/filepath" "slices" - "strconv" - "strings" "github.com/djherbis/times" ) @@ -114,10 +112,7 @@ func copyAll(srcs []string, dstDir string, preserve []string) (nums chan int64, basename := file[:len(file)-len(ext)] var newPath string for i := 1; !os.IsNotExist(err); i++ { - file = strings.ReplaceAll(gOpts.dupfilefmt, "%f", basename+ext) - file = strings.ReplaceAll(file, "%b", basename) - file = strings.ReplaceAll(file, "%e", ext) - file = strings.ReplaceAll(file, "%n", strconv.Itoa(i)) + file = formatDuplicatedFilename(basename, ext, i) newPath = filepath.Join(dstDir, file) _, err = os.Lstat(newPath) } diff --git a/doc.md b/doc.md index aed366bb..1eb4ae7d 100644 --- a/doc.md +++ b/doc.md @@ -285,6 +285,7 @@ The following options can be used to customize the behavior of lf: info []string (default '') infotimefmtnew string (default 'Jan _2 15:04') infotimefmtold string (default 'Jan _2 2006') + luamsglog bool (default false) menufmt string (default "\033[0m") menuheaderfmt string (default "\033[1m") menuselectfmt string (default "\033[7m") @@ -1023,6 +1024,10 @@ Format string of the file time shown in the info column when it matches this yea Format string of the file time shown in the info column when it doesn't match this year. +## luamsglog (bool) (default false) + +Log each call of Lua message, this option can be used for debug purpose. + ## menufmt (string) (default `\033[0m`) Format string of the menu. @@ -1077,7 +1082,7 @@ Note that preserving other attributes like ownership or change/birth timestamps Show previews of files and directories at the rightmost pane. If the file has more lines than the preview pane, the rest of the lines are not read. -Files are considered binary and displayed as `binary` if the read portion contains a control character other than bell, backspace, tab, newline, vertical tab, form feed, carriage return, escape or delete. +Files are considered binary and displayed as `binary` if the read portion contains a control character other than bell, backspace, tab, newline, vertical tab, form feed, carriage return, escape or delete. ## previewer (string) (default ``) (not filtered if empty) diff --git a/eval.go b/eval.go index 07b15709..5f65263c 100644 --- a/eval.go +++ b/eval.go @@ -16,6 +16,7 @@ import ( "github.com/clipperhouse/displaywidth" "github.com/gdamore/tcell/v3" + lua "github.com/yuin/gopher-lua" ) func applyBoolOpt(opt *bool, e *setExpr) error { @@ -121,6 +122,8 @@ func (e *setExpr) eval(app *app, _ []string) { err = applyBoolOpt(&gOpts.incfilter, e) case "incsearch", "noincsearch", "incsearch!": err = applyBoolOpt(&gOpts.incsearch, e) + case "luamsglog", "noluamsglog", "luamsglog!": + err = applyBoolOpt(&gOpts.luamsglog, e) case "mergeindicators", "nomergeindicators", "mergeindicators!": err = applyBoolOpt(&gOpts.mergeindicators, e) case "mouse", "nomouse", "mouse!": @@ -639,60 +642,106 @@ func (e *cmdExpr) eval(app *app, _ []string) { } } +func (e *luaMsgExpr) eval(app *app, args []string) { + _, err := callLuaMsgExpr(e, func(L *lua.LState) []lua.LValue { + msgArgs := make([]lua.LValue, len(args)) + for i, arg := range args { + msgArgs[i] = lua.LString(arg) + } + return msgArgs + }) + + if err != nil { + app.ui.echoerrf("Lua msg execution failed, see log for more detail") + log.Println(err) + } +} + +func (e *luaKeyMapExpr) eval(app *app, _ []string) { + err := callLuaKeyMapMsg(e) + if err != nil { + app.ui.echoerrf("Lua key map error (%s): %s", e, err) + } +} + func preChdir(app *app) { - if cmd, ok := gOpts.cmds["pre-cd"]; ok { + cmdName := "pre-cd" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func onChdir(app *app) { app.nav.addJumpList() - if cmd, ok := gOpts.cmds["on-cd"]; ok { + cmdName := "on-cd" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func onLoad(app *app, files []string) { - if cmd, ok := gOpts.cmds["on-load"]; ok { + cmdName := "on-load" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, files) } + callLuaEventHooks(cmdName, func(L *lua.LState) []lua.LValue { + args := make([]lua.LValue, len(files)) + for i, f := range files { + args[i] = lua.LString(f) + } + return args + }) } func onFocusGained(app *app) { - if cmd, ok := gOpts.cmds["on-focus-gained"]; ok { + cmdName := "on-focus-gained" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func onFocusLost(app *app) { - if cmd, ok := gOpts.cmds["on-focus-lost"]; ok { + cmdName := "on-focus-lost" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func onInit(app *app) { - if cmd, ok := gOpts.cmds["on-init"]; ok { + cmdName := "on-init" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func onRedraw(app *app) { - if cmd, ok := gOpts.cmds["on-redraw"]; ok { + cmdName := "on-redraw" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func onSelect(app *app) { app.nav.preload() - if cmd, ok := gOpts.cmds["on-select"]; ok { + cmdName := "on-select" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func onQuit(app *app) { - if cmd, ok := gOpts.cmds["on-quit"]; ok { + cmdName := "on-quit" + if cmd, ok := gOpts.cmds[cmdName]; ok { cmd.eval(app, nil) } + callLuaEventHooks(cmdName, nil) } func splitKeys(s string) (keys []string) { @@ -1669,6 +1718,26 @@ func (e *callExpr) eval(app *app, _ []string) { clear(gOpts.vkeys) gOpts.nkeys[":"] = &callExpr{"read", nil, 1} gOpts.vkeys[":"] = &callExpr{"read", nil, 1} + case "luapreviewer-priority": + argc := len(e.args) + if argc%2 != 0 { + app.ui.echoerr("luapreviewer-priority: requires an even number of arguments") + return + } + + changed := false + for i := 0; i < argc; i += 2 { + name, priorityStr := e.args[i], e.args[i+1] + priority, err := strconv.Atoi(priorityStr) + if err == nil { + withSort := i+2 >= argc && changed + changed = setLuaPreviewerPriority(name, priority, withSort) || changed + } else { + app.ui.echoerrf("luapreviewer-priority: invalid priority for %s: %s", name, priorityStr) + } + } + case "plugin-reload": + luaPluginReload(app) case "tty-write": if len(e.args) != 1 { app.ui.echoerr("tty-write: requires an argument") diff --git a/go.mod b/go.mod index 4ae0edef..0bd32940 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/djherbis/times v1.6.0 github.com/fsnotify/fsnotify v1.10.1 github.com/gdamore/tcell/v3 v3.4.0 + github.com/yuin/gopher-lua v1.1.2 golang.org/x/sys v0.46.0 golang.org/x/term v0.44.0 ) diff --git a/go.sum b/go.sum index 85668316..4423b856 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/gdamore/tcell/v3 v3.4.0/go.mod h1:fjKxNiIFwbzTxDU+i+AAMz+xPOgXVaZq5tb github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= +github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/lua_binding_bufio.go b/lua_binding_bufio.go new file mode 100644 index 00000000..60147f58 --- /dev/null +++ b/lua_binding_bufio.go @@ -0,0 +1,274 @@ +package main + +import ( + "bufio" + + lua "github.com/yuin/gopher-lua" +) + +// ---------------------------------------------------------------------------- +// Type bufio.Writer + +const luaBufWriterTypeName = "bufio.Writer" + +func lRegisterBufWriterType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaBufWriterTypeName) + + // L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "available": luaBufWriterAvailable, + "buffered": luaBufWriterBuffered, + "flush": luaBufWriterFlush, + "size": luaBufWriterSize, + "write_string": luaBufWriterWriteString, + })) + + return mt +} + +func lCheckBufWriter(L *lua.LState, index int) *bufio.Writer { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*bufio.Writer); ok { + return v + } + + L.ArgError(index, "value of type `BufWriter` expected") + + return nil +} + +func lWrapBufWriter(L *lua.LState, data *bufio.Writer) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaBufWriterTypeName)) + + return ud +} + +func lAddBufWriterToState(L *lua.LState, data *bufio.Writer) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapBufWriter(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaBufWriterAvailable returns available byte count in buffer. +func luaBufWriterAvailable(L *lua.LState) int { + writer := lCheckBufWriter(L, 1) + L.Push(lua.LNumber(writer.Available())) + return 1 +} + +// luaBufWriterBuffered returns number of bytes that has been written to buffer. +func luaBufWriterBuffered(L *lua.LState) int { + writer := lCheckBufWriter(L, 1) + L.Push(lua.LNumber(writer.Buffered())) + return 1 +} + +// luaBufWriterFlush writes buffered data to underlying output writer. +func luaBufWriterFlush(L *lua.LState) int { + writer := lCheckBufWriter(L, 1) + err := writer.Flush() + + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +// luaBufWriterSize returns byte size of underlying buffer. +func luaBufWriterSize(L *lua.LState) int { + writer := lCheckBufWriter(L, 1) + L.Push(lua.LNumber(writer.Size())) + return 1 +} + +// luaBufWriterWriteString writes string value to buffer. +func luaBufWriterWriteString(L *lua.LState) int { + writer := lCheckBufWriter(L, 1) + + nArgs := L.GetTop() + sum := 0 + for i := 2; i <= nArgs; i++ { + str := L.CheckString(i) + n, err := writer.WriteString(str) + sum += n + + if err != nil { + L.Push(lua.LNumber(sum)) + L.Push(lua.LString(err.Error())) + return 2 + } + } + + L.Push(lua.LNumber(sum)) + + return 1 +} + +// ---------------------------------------------------------------------------- +// type bufio.Reader + +const luaBufReaderTypeName = "bufio.Reader" + +func lRegisterBufReaderType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaBufReaderTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "buffered": luaBufReaderBuffered, + "discard": luaBufReaderDiscard, + "peek": luaBufReaderPeek, + "read": luaBufReaderRead, + "read_line": luaBufReaderReadLine, + "read_string": luaBufReaderReadString, + "size": luaBufReaderSize, + })) + + return mt +} + +func lCheckBufReader(L *lua.LState, index int) *bufio.Reader { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*bufio.Reader); ok { + return v + } + + L.ArgError(index, "value of type `BufReader` expected") + + return nil +} + +func lWrapBufReader(L *lua.LState, data *bufio.Reader) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaBufReaderTypeName)) + + return ud +} + +func lAddBufReaderToState(L *lua.LState, data *bufio.Reader) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapBufReader(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaBufReaderBuffered returns number of bytes buffered. +func luaBufReaderBuffered(L *lua.LState) int { + reader := lCheckBufReader(L, 1) + L.Push(lua.LNumber(reader.Buffered())) + return 1 +} + +// luaBufReaderDiscard skips following n bytes, returns number of bytes skipped. +func luaBufReaderDiscard(L *lua.LState) int { + reader := lCheckBufReader(L, 1) + n := L.CheckInt(2) + + discarded, err := reader.Discard(n) + L.Push(lua.LNumber(discarded)) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +// luaBufReaderPeek returns next n bytes without advancing reader. +func luaBufReaderPeek(L *lua.LState) int { + reader := lCheckBufReader(L, 1) + n := L.CheckInt(2) + + buf, err := reader.Peek(n) + L.Push(lua.LString(string(buf))) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +// luaBufReaderRead reads n bytes from reader, and returns datga as a string. +func luaBufReaderRead(L *lua.LState) int { + reader := lCheckBufReader(L, 1) + n := L.CheckInt(2) + + buf := make([]byte, n) + nRead, err := reader.Read(buf) + + L.Push(lua.LString(string(buf[:nRead]))) + L.Push(lua.LNumber(nRead)) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 3 + } + + return 2 +} + +// luaBufReaderReadLine reads one line of data. Returned data will not contain +// trailing `\r\n` or `\n` +func luaBufReaderReadLine(L *lua.LState) int { + reader := lCheckBufReader(L, 1) + + line, isPrefix, err := reader.ReadLine() + + L.Push(lua.LString(string(line))) + L.Push(lua.LBool(isPrefix)) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 3 + } + + return 2 +} + +// luaBufReaderReadString takes a delimiter string, and reads until that string +// occurs. +func luaBufReaderReadString(L *lua.LState) int { + reader := lCheckBufReader(L, 1) + delim := L.CheckString(2) + + str, err := reader.ReadString(delim[0]) + L.Push(lua.LString(str)) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +// luaBufReaderSize returns byte size of underlying buffer. +func luaBufReaderSize(L *lua.LState) int { + reader := lCheckBufReader(L, 1) + L.Push(lua.LNumber(reader.Size())) + return 1 +} diff --git a/lua_binding_exec.go b/lua_binding_exec.go new file mode 100644 index 00000000..127d95b7 --- /dev/null +++ b/lua_binding_exec.go @@ -0,0 +1,363 @@ +package main + +import ( + "bufio" + "io" + "os/exec" + + lua "github.com/yuin/gopher-lua" +) + +// ---------------------------------------------------------------------------- +// type exec.Cmd + +const luaCmdTypeName = "exec.Cmd" + +func lRegisterCmdType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaCmdTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaCmdNew, + "__tostring": luaCmdMetaTostring, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "environ": luaCmdEnviron, + "add_environ": luaCmdAddEnviron, + + "combined_output": luaCmdCombinedOutput, + "output": luaCmdOutput, + "run": luaCmdRun, + + "start": luaCmdStart, + "wait": luaCmdWait, + + "stderr_pipe": luaCmdStrerrPipe, + "stdout_pipe": luaCmdStdoutPipe, + "stdin_pipe": luaCmdStdinPipe, + + "exit_code": luaCmdExitCode, + + "set_stdout_writer": luaCmdSetStdoutWriter, + "set_stderr_writer": luaCmdSetStderrWriter, + "set_stdout_writer_func": luaCmdSetStdoutWriterFunc, + "set_stderr_writer_func": luaCmdSetStderrWriterFunc, + + "kill": luaCmdKill, + })) + + return mt +} + +func lCheckCmd(L *lua.LState, index int) *exec.Cmd { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*exec.Cmd); ok { + return v + } + + L.ArgError(index, "value of type `Cmd` expected") + + return nil +} + +func lWrapCmd(L *lua.LState, data *exec.Cmd) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaCmdTypeName)) + + return ud +} + +func LAddCmdToState(L *lua.LState, data *exec.Cmd) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapCmd(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaCmdNew creates a new command object. +func luaCmdNew(L *lua.LState) int { + cmdStr := L.CheckString(1) + + st := 2 + nArgs := L.GetTop() + args := make([]string, nArgs-st+1) + for i := st; i <= nArgs; i++ { + args[i-st] = L.Get(i).String() + } + + cmd := exec.Command(cmdStr, args...) + + return LAddCmdToState(L, cmd) +} + +func luaCmdMetaTostring(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + L.Push(lua.LString(cmd.String())) + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaCmdEnviron is a getter & setter for environment variable list of Cmd. +// When used as a getter, it returns a copy of command's environment variable +// list as table. +// Every environment variable is set in form of a `=` string. +func luaCmdEnviron(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + if L.GetTop() >= 2 { + kvList := L.CheckTable(2) + nElem := kvList.Len() + env := make([]string, nElem) + + for i := 0; i < nElem; i++ { + env[i] = kvList.RawGetInt(i + 1).String() + } + cmd.Env = env + + L.Push(kvList) + + return 1 + } + + env := cmd.Environ() + + envTable := L.NewTable() + for _, kv := range env { + envTable.Append(lua.LString(kv)) + } + + L.Push(envTable) + + return 1 +} + +// luaCmdAddEnviron appends new key-value string to command's environment variable +// list. +func luaCmdAddEnviron(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + kv := L.CheckString(2) + + cmd.Env = append(cmd.Env, kv) + + return 0 +} + +// luaCmdCombinedOutput runs command and returns string result containing both +// stdout and stderr output. +func luaCmdCombinedOutput(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + output, err := cmd.CombinedOutput() + L.Push(lua.LString(string(output))) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +// luaCmdOutput runs command and returns string result containing only stdout +// output. +func luaCmdOutput(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + output, err := cmd.Output() + L.Push(lua.LString(string(output))) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +// luaCmdRun runs current command. +func luaCmdRun(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + err := cmd.Run() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +// luaCmdStart stars execution command. Caller should then calls `wait` method +// to wait for execution ends. +func luaCmdStart(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + err := cmd.Start() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +// luaCmdWait blocks execution until execution of command ends. +func luaCmdWait(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + err := cmd.Wait() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +// luaCmdStrerrPipe returns a reader handle to command's stderr output. This should +// be called before command starts execution. +func luaCmdStrerrPipe(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + stderrPipe, err := cmd.StderrPipe() + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + reader := bufio.NewReader(stderrPipe) + + return lAddBufReaderToState(L, reader) +} + +// luaCmdStdoutPipe returns a reader handle to command's stdout output. This should +// be called before command starts execution. +func luaCmdStdoutPipe(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + reader := bufio.NewReader(stdoutPipe) + + return lAddBufReaderToState(L, reader) +} + +// luaCmdStdinPipe returns a writer handle to command's stdin input. This should +// be called before command starts execution. +func luaCmdStdinPipe(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + stdinPipe, err := cmd.StdinPipe() + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + writer := bufio.NewWriter(stdinPipe) + + return lAddBufWriterToState(L, writer) +} + +// luaCmdExitCode returns exit code of finished command. When exit code is not +// available, this method returns `nil`. +func luaCmdExitCode(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + + if cmd.ProcessState == nil { + return 0 + } + + exitCode := cmd.ProcessState.ExitCode() + + L.Push(lua.LNumber(exitCode)) + + return 1 +} + +// luaCmdSetStdoutWriter sets a writer value for command stdout. +func luaCmdSetStdoutWriter(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + ud := L.CheckUserData(2) + + writer, ok := ud.Value.(io.Writer) + if !ok { + L.ArgError(2, "is not a writer") + } + + cmd.Stdout = writer + + return 0 +} + +// luaCmdSetStderrWriter sets a writer value for command stdout. +func luaCmdSetStderrWriter(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + ud := L.CheckUserData(2) + + writer, ok := ud.Value.(io.Writer) + if !ok { + L.ArgError(2, "is not a writer") + } + + cmd.Stderr = writer + + return 0 +} + +// luaCmdSetStdoutWriterFunc sets a Lua function as writer used for command stdout. +func luaCmdSetStdoutWriterFunc(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + fn := L.CheckFunction(2) + + writer := &luaFuncWriter{ + luaState: L, + fn: fn, + } + + cmd.Stdout = writer + + return 0 +} + +// luaCmdSetStderrWriterFunc sets a Lua function as writer used for command stderr. +func luaCmdSetStderrWriterFunc(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + fn := L.CheckFunction(2) + + writer := &luaFuncWriter{ + luaState: L, + fn: fn, + } + + cmd.Stderr = writer + + return 0 +} + +func luaCmdKill(L *lua.LState) int { + cmd := lCheckCmd(L, 1) + err := cmd.Process.Kill() + + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} diff --git a/lua_binding_fs.go b/lua_binding_fs.go new file mode 100644 index 00000000..88a24bee --- /dev/null +++ b/lua_binding_fs.go @@ -0,0 +1,286 @@ +package main + +import ( + "io/fs" + + lua "github.com/yuin/gopher-lua" +) + +// ---------------------------------------------------------------------------- +// Type fs.FileInfo + +const luaFileInfoTypeName = "fs.FileInfo" + +func lRegisterFileInfoType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaFileInfoTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "name": luaFileInfoName, + "size": luaFileInfoSize, + "mode": luaFileInfoMode, + "mod_time": luaFileInfoModTime, + "is_dir": luaFileInfoIsDir, + })) + + return mt +} + +func lCheckFileInfo(L *lua.LState, index int) fs.FileInfo { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(fs.FileInfo); ok { + return v + } + + L.ArgError(index, "value of type `FileInfo` expected") + + return nil +} + +func lWrapFileInfo(L *lua.LState, data fs.FileInfo) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaFileInfoTypeName)) + + return ud +} + +func lAddFileInfoToState(L *lua.LState, data fs.FileInfo) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapFileInfo(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaFileInfoName returns base name of file. +func luaFileInfoName(L *lua.LState) int { + info := lCheckFileInfo(L, 1) + L.Push(lua.LString(info.Name())) + return 1 +} + +// luaFileInfoSize returns length in bytes for regular files +func luaFileInfoSize(L *lua.LState) int { + info := lCheckFileInfo(L, 1) + L.Push(lua.LNumber(info.Size())) + return 1 +} + +// luaFileInfoMode returns mode bits userdata of this file. +func luaFileInfoMode(L *lua.LState) int { + info := lCheckFileInfo(L, 1) + return lAddFileModeToState(L, info.Mode()) +} + +// luaFileInfoModTime returns modification time of file. +func luaFileInfoModTime(L *lua.LState) int { + info := lCheckFileInfo(L, 1) + t := info.ModTime() + return lAddTimeToState(L, &t) +} + +// luaFileInfoIsDir returns if this file is a directory. +func luaFileInfoIsDir(L *lua.LState) int { + info := lCheckFileInfo(L, 1) + L.Push(lua.LBool(info.IsDir())) + return 1 +} + +// ---------------------------------------------------------------------------- +// type fs.DirEntry + +const luaDirEntryTypeName = "fs.DirEntry" + +func lRegisterDirEntryType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaDirEntryTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "name": luaDirEntryName, + "info": luaDirEntryInfo, + "is_dir": luaDirEntryIsDir, + "type": luaDirEntryType, + })) + + return mt +} + +func lCheckDirEntry(L *lua.LState, index int) fs.DirEntry { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(fs.DirEntry); ok { + return v + } + + L.ArgError(index, "value of type `DirEntry` expected") + + return nil +} + +func lWrapDirEntry(L *lua.LState, data fs.DirEntry) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaDirEntryTypeName)) + + return ud +} + +/* func lAddDirEntryToState(L *lua.LState, data fs.DirEntry) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapDirEntry(L, data) + L.Push(ud) + + return 1 +} */ + +// ---------------------------------------------------------------------------- + +// luaDirEntryName returns name of this entry. +func luaDirEntryName(L *lua.LState) int { + entry := lCheckDirEntry(L, 1) + L.Push(lua.LString(entry.Name())) + return 1 +} + +// luaDirEntryInfo returns FileInfo of this entry. +func luaDirEntryInfo(L *lua.LState) int { + entry := lCheckDirEntry(L, 1) + info, err := entry.Info() + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + return lAddFileInfoToState(L, info) +} + +// luaDirEntryIsDir returns true if this entry is directory. +func luaDirEntryIsDir(L *lua.LState) int { + entry := lCheckDirEntry(L, 1) + L.Push(lua.LBool(entry.IsDir())) + return 1 +} + +// luaDirEntryType returns the type bits for the entry. This is a subset of the +// usual FileMode bits. +func luaDirEntryType(L *lua.LState) int { + entry := lCheckDirEntry(L, 1) + return lAddFileModeToState(L, entry.Type()) +} + +// ---------------------------------------------------------------------------- +// type fs.FileMode + +const luaFileModeTypeName = "fs.FileMode" + +func lRegisterFileModeType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaFileModeTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaFileModeNew, + "__tostring": luaFileModeMetaTostring, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "is_dir": luaFileModeIsDir, + "is_regular": luaFileModeIsRegular, + "perm": luaFileModePerm, + "type": luaFileModeType, + + "to_number": luaFileModeToNumber, + })) + + return mt +} + +func lCheckFileMode(L *lua.LState, index int) fs.FileMode { + value := L.Get(index) + switch value.Type() { + case lua.LTNumber: + dur := fs.FileMode(value.(lua.LNumber)) + return dur + case lua.LTUserData: + if v, ok := value.(*lua.LUserData).Value.(fs.FileMode); ok { + return v + } + } + + L.ArgError(index, "value of type `FileMode` expected") + + return 0 +} + +func lWrapFileMode(L *lua.LState, data fs.FileMode) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaFileModeTypeName)) + + return ud +} + +func lAddFileModeToState(L *lua.LState, data fs.FileMode) int { + ud := lWrapFileMode(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaFileModeNew(L *lua.LState) int { + value := L.CheckInt(1) + return lAddFileModeToState(L, fs.FileMode(value)) +} + +func luaFileModeMetaTostring(L *lua.LState) int { + mode := lCheckFileMode(L, 1) + L.Push(lua.LString(mode.String())) + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaFileModeIsDir returns true if current file mode is directory. +func luaFileModeIsDir(L *lua.LState) int { + mode := lCheckFileMode(L, 1) + L.Push(lua.LBool(mode.IsDir())) + return 1 +} + +// luaFileModeIsRegular returns true if current file mode is regular file. +func luaFileModeIsRegular(L *lua.LState) int { + mode := lCheckFileMode(L, 1) + L.Push(lua.LBool(mode.IsRegular())) + return 1 +} + +// luaFileModePerm returns unix permission bits in mode. +func luaFileModePerm(L *lua.LState) int { + mode := lCheckFileMode(L, 1) + return lAddFileModeToState(L, mode.Perm()) +} + +// luaFileModeType returns type bits in mode. +func luaFileModeType(L *lua.LState) int { + mode := lCheckFileMode(L, 1) + return lAddFileModeToState(L, mode.Type()) +} + +// luaFileModeToNumber converts FileMode userdata to number. +func luaFileModeToNumber(L *lua.LState) int { + mode := lCheckFileMode(L, 1) + L.Push(lua.LNumber(mode)) + return 1 +} diff --git a/lua_binding_main.go b/lua_binding_main.go new file mode 100644 index 00000000..c5953200 --- /dev/null +++ b/lua_binding_main.go @@ -0,0 +1,3378 @@ +package main + +import ( + "fmt" + "log" + "slices" + + lua "github.com/yuin/gopher-lua" +) + +// ---------------------------------------------------------------------------- +// Type app + +const luaAppTypeName = "lf.app" + +func lRegisterAppType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaAppTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "ui": luaAppUI, + "nav": luaAppNav, + + "read_file": luaAppReadFile, + })) + + return mt +} + +func lCheckApp(L *lua.LState, index int) *app { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*app); ok { + return v + } + + L.ArgError(index, "value of type `App` expected") + + return nil +} + +func lWrapApp(L *lua.LState, data *app) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaAppTypeName)) + + return ud +} + +/* func lAddAppToState(L *lua.LState, data *app) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapApp(L, data) + L.Push(ud) + + return 1 +} */ + +// ---------------------------------------------------------------------------- + +// luaAppUI returns `ui` object hold by app +func luaAppUI(L *lua.LState) int { + app := lCheckApp(L, 1) + return lAddUIToState(L, app.ui) +} + +// luaAppNav returns `nav` object hold by app +func luaAppNav(L *lua.LState) int { + app := lCheckApp(L, 1) + return lAddNavToState(L, app.nav) +} + +// luaAppReadFile reads specified config file. +func luaAppReadFile(L *lua.LState) int { + app := lCheckApp(L, 1) + path := L.CheckString(2) + app.readFile(path) + return 0 +} + +// ---------------------------------------------------------------------------- +// Type compMatch + +const luaCompMatchTypeName = "lf.compMatch" + +func lRegisterCompMatchType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaCompMatchTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaCompMatchNew, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "name": luaCompMatchName, + "result": luaCompMatchResult, + })) + + return mt +} + +func lCheckCompMatch(L *lua.LState, index int) *compMatch { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*compMatch); ok { + return v + } + + L.ArgError(index, "value of type `CompMatch` expected") + + return nil +} + +func lWrapCompMatch(L *lua.LState, data *compMatch) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaCompMatchTypeName)) + + return ud +} + +func lAddCompMatchToState(L *lua.LState, data *compMatch) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapCompMatch(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaCompMatchNew(L *lua.LState) int { + name := L.CheckString(1) + result := L.CheckString(2) + return lAddCompMatchToState(L, &compMatch{name: name, result: result}) +} + +// ---------------------------------------------------------------------------- + +// luaCompMatchName is getter & setter for name field. It's displayed text for +// this completion entry. +func luaCompMatchName(L *lua.LState) int { + cm := lCheckCompMatch(L, 1) + + if L.GetTop() >= 2 { + value := L.CheckString(2) + cm.name = value + } + + L.Push(lua.LString(cm.name)) + + return 1 +} + +// luaCompMatchResult is getter & setter for result field. It's applied text used +// when this completion entry is picked. +func luaCompMatchResult(L *lua.LState) int { + cm := lCheckCompMatch(L, 1) + + if L.GetTop() >= 2 { + value := L.CheckString(2) + cm.result = value + } + + L.Push(lua.LString(cm.result)) + + return 1 +} + +// ---------------------------------------------------------------------------- +// Type file + +const FileTypeName = "lf.file" + +func lRegisterFileTypeMt(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(FileTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaFileNew, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "name": luaFileName, + "size": luaFileSize, + "mode": luaFileMode, + "mod_time": luaFileModTime, + "is_dir": luaFileIsDir, + + "link_state": luaFileLinkState, + "link_target": luaFileLinkTarget, + "path": luaFilePath, + + "dir_count": luaFileDirCount, + "dir_size": luaFileDirSize, + + "access_time": luaFileAccessTime, + "birth_time": luaFileBirthTime, + "change_time": luaFileChangeTime, + + "custom_info": luaFileCustomInfo, + "ext": luaFileExt, + + "extra_data": luaFileExtraData, + "extra_data_keys": luaFileExtraDataKeys, + + "is_previewable": luaFileIsPreviewable, + })) + + addLinkStateConstantToMt(L, mt) + + return mt +} + +func addLinkStateConstantToMt(L *lua.LState, tbl *lua.LTable) { + L.SetField(tbl, "LinkStateNotLink", lua.LNumber(notLink)) + L.SetField(tbl, "LinkStateWorking", lua.LNumber(working)) + L.SetField(tbl, "LinkStateBroken", lua.LNumber(broken)) +} + +func lCheckFile(L *lua.LState, index int) *file { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*file); ok { + return v + } + + L.ArgError(index, "value of type `File` expected") + + return nil +} + +func lWrapFile(L *lua.LState, data *file) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(FileTypeName)) + + return ud +} + +func lAddFileToState(L *lua.LState, data *file) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapFile(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaFileNew(L *lua.LState) int { + path := L.CheckString(1) + file := newFile(path) + return lAddFileToState(L, file) +} + +// ---------------------------------------------------------------------------- + +func luaFileName(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LString(file.Name())) + return 1 +} + +func luaFileSize(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LNumber(file.Size())) + return 1 +} + +func luaFileMode(L *lua.LState) int { + file := lCheckFile(L, 1) + return lAddFileModeToState(L, file.Mode()) +} + +func luaFileModTime(L *lua.LState) int { + file := lCheckFile(L, 1) + modTime := file.ModTime() + return lAddTimeToState(L, &modTime) +} + +func luaFileIsDir(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LBool(file.IsDir())) + return 1 +} + +func luaFileLinkState(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LNumber(file.linkState)) + return 1 +} + +func luaFileLinkTarget(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LString(file.linkTarget)) + return 1 +} + +func luaFilePath(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LString(file.path)) + return 1 +} + +// luaFileDirCount returns number items of a directory. +func luaFileDirCount(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LNumber(file.dirCount)) + return 1 +} + +// luaFileDirSize return directory's total content size. +func luaFileDirSize(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LNumber(file.dirSize)) + return 1 +} + +func luaFileAccessTime(L *lua.LState) int { + file := lCheckFile(L, 1) + return lAddTimeToState(L, &file.accessTime) +} + +func luaFileBirthTime(L *lua.LState) int { + file := lCheckFile(L, 1) + return lAddTimeToState(L, &file.birthTime) +} + +func luaFileChangeTime(L *lua.LState) int { + file := lCheckFile(L, 1) + return lAddTimeToState(L, &file.changeTime) +} + +// luaFileCustomInfo is a getter and setter for custom info string added to this +// file by `addcustominfo` command. +func luaFileCustomInfo(L *lua.LState) int { + file := lCheckFile(L, 1) + + if L.GetTop() >= 2 { + tryRaiseNonSyncLuaStateError(L) + value := L.CheckString(2) + file.customInfo = value + } + + L.Push(lua.LString(file.customInfo)) + + return 1 +} + +func luaFileExt(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LString(file.ext)) + return 1 +} + +// luaFileExtraData can get & stores value to a map associated with this file. +// Only number, string, boolean, nil value are supported. +func luaFileExtraData(L *lua.LState) int { + file := lCheckFile(L, 1) + key := L.CheckString(2) + + nargs := L.GetTop() + if nargs >= 3 { + tryRaiseNonSyncLuaStateError(L) + value := L.Get(3) + + if file.extraLuaData == nil { + file.extraLuaData = make(map[string]any) + } + + if value == lua.LNil { + delete(file.extraLuaData, key) + return 0 + } + + goValue, err := luaPlainValueToGoValue(value) + if err != nil { + L.Push(value) + L.Push(lua.LString(err.Error())) + return 2 + } + + file.extraLuaData[key] = goValue + L.Push(value) + + return 1 + } + + if file.extraLuaData == nil { + L.Push(lua.LNil) + return 1 + } + + goValue := file.extraLuaData[key] + value, err := goValueToLuaValue(L, goValue) + + L.Push(value) + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +// luaFileExtraDataKeys returns list of all keys in extra data map. +func luaFileExtraDataKeys(L *lua.LState) int { + file := lCheckFile(L, 1) + + tbl := L.NewTable() + for k := range file.extraLuaData { + tbl.Append(lua.LString(k)) + } + + L.Push(tbl) + + return 1 +} + +// luaFileIsPreviewable returns true if this file requires a preview call. +func luaFileIsPreviewable(L *lua.LState) int { + file := lCheckFile(L, 1) + if file.isPreviewablePlain() { + L.Push(lua.LTrue) + return 1 + } + + previewer := getLuaPreviewerForPathOnState(L, file.path) + + L.Push(lua.LBool(previewer != nil)) + + return 1 +} + +// ---------------------------------------------------------------------------- +// Type dir + +const luaDirTypeName = "lf.dir" + +func lRegisterDirType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaDirTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaDirNew, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "loading": luaDirLoading, + "load_time": luaDirLoadTime, + "ind": luaDirInd, + "pos": luaDirPos, + "path": luaDirPath, + + "files": luaDirFiles, + "files_len": luaDirFilesLen, + "files_get_index": luaDirFilesGetIndex, + "iter_files": luaDirIterFiles, + "all_files": luaDirAllFiles, + "all_files_len": luaDirAllFilesLen, + "all_files_get_index": luaDirAllFilesGetIndex, + "iter_all_files": luaDirIterAllFiles, + + "sortby": luaDirSortby, + "dircounts": luaDirDircounts, + "dirfirst": luaLuaDirfirst, + "dironly": luaDirDironly, + "hidden": luaDirHidden, + "reverse": luaDirReverse, + "visual_anchor": luaDirVisualAnchor, + "visual_wrap": luaDirVisualWrap, + "hiddenfiles": luaDirHiddenFiles, + "filter": luaDirFilter, + "filter_len": luaDirFilterLen, + "filter_get_index": luaDirFilterGetIndex, + "iter_filters": luaDirIterFilters, + "sortignorecase": luaDirSortignorecase, + "sortignoredia": luaDirSortignoredia, + "no_perm": luaDirNoPerm, + + "sort": luaDirSort, + "name": luaDirName, + "visual_selections": luaDirVisualSelectioins, + "sel": luaDirSel, + "bound_pos": luaDirBoundPos, + + "extra_data": luaDirExtraData, + "extra_data_keys": luaDirExtraDataKeys, + })) + + return mt +} + +func lCheckDir(L *lua.LState, index int) *dir { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*dir); ok { + return v + } + + L.ArgError(index, "value of type `Dir` expected") + + return nil +} + +func lWrapDir(L *lua.LState, data *dir) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaDirTypeName)) + + return ud +} + +func lAddDirToState(L *lua.LState, data *dir) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapDir(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaDirNew(L *lua.LState) int { + path := L.CheckString(1) + dir := newDir(path) + return lAddDirToState(L, dir) +} + +// ---------------------------------------------------------------------------- + +func luaDirLoading(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.loading)) + return 1 +} + +func luaDirLoadTime(L *lua.LState) int { + dir := lCheckDir(L, 1) + return lAddTimeToState(L, &dir.loadTime) +} + +// luaDirInd is getter & setter for 0-based index of current entry in directory. +func luaDirInd(L *lua.LState) int { + dir := lCheckDir(L, 1) + + if L.GetTop() > 1 { + tryRaiseNonSyncLuaStateError(L) + + value := L.CheckInt(2) + if value < 0 || value >= len(dir.files) { + L.ArgError(2, fmt.Sprintf("index out of range: %d (max index %d)", value, len(dir.files))) + } + + dir.ind = value + } + + L.Push(lua.LNumber(dir.ind)) + return 1 +} + +// luaDirPos is getter & setter for 0-based row index of cursor position in directory +// window. +func luaDirPos(L *lua.LState) int { + dir := lCheckDir(L, 1) + + if L.GetTop() > 1 { + tryRaiseNonSyncLuaStateError(L) + + value := L.CheckInt(2) + dir.pos = value + } + + L.Push(lua.LNumber(dir.pos)) + return 1 +} + +func luaDirPath(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LString(dir.path)) + return 1 +} + +// luaDirFiles returns a list of displayed file. +func luaDirFiles(L *lua.LState) int { + dir := lCheckDir(L, 1) + + filesTable := L.NewTable() + for _, file := range dir.files { + filesTable.Append(lWrapFile(L, file)) + } + + L.Push(filesTable) + + return 1 +} + +func luaDirFilesLen(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LNumber(len(dir.files))) + return 1 +} + +func luaDirFilesGetIndex(L *lua.LState) int { + dir := lCheckDir(L, 1) + index := L.CheckInt(2) + if index <= 0 || index > len(dir.files) { + L.ArgError(2, fmt.Sprintf("index out of range: %d (max index %d)", index, len(dir.files))) + } + return lAddFileToState(L, dir.files[index-1]) +} + +// luaDirIterFiles returns iterator over displayed files. +func luaDirIterFiles(L *lua.LState) int { + dir := lCheckDir(L, 1) + + L.Push(L.NewFunction(func(L *lua.LState) int { + ud := L.CheckUserData(1) + index := L.CheckInt(2) + + list, ok := ud.Value.([]*file) + if !ok { + L.Push(lua.LNil) + return 1 + } + + if index >= len(list) { + L.Push(lua.LNil) + return 1 + } + + L.Push(lua.LNumber(index + 1)) + L.Push(lWrapFile(L, list[index])) + + return 2 + })) + + ud := L.NewUserData() + ud.Value = dir.files + + L.Push(ud) + L.Push(lua.LNumber(0)) + + return 3 +} + +// luaDirAllFiles returns a list of file including non-displayed ones. +func luaDirAllFiles(L *lua.LState) int { + dir := lCheckDir(L, 1) + + filesTable := L.NewTable() + for _, file := range dir.allFiles { + filesTable.Append(lWrapFile(L, file)) + } + + L.Push(filesTable) + + return 1 +} + +func luaDirAllFilesLen(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LNumber(len(dir.allFiles))) + return 1 +} + +func luaDirAllFilesGetIndex(L *lua.LState) int { + dir := lCheckDir(L, 1) + index := L.CheckInt(2) + if index <= 0 || index > len(dir.allFiles) { + L.ArgError(2, fmt.Sprintf("index out of range: %d (max index %d)", index, len(dir.allFiles))) + } + return lAddFileToState(L, dir.allFiles[index-1]) +} + +// luaDirIterAllFiles returns iterator over all files. +func luaDirIterAllFiles(L *lua.LState) int { + dir := lCheckDir(L, 1) + + L.Push(L.NewFunction(func(L *lua.LState) int { + ud := L.CheckUserData(1) + index := L.CheckInt(2) + + list, ok := ud.Value.([]*file) + if !ok { + L.Push(lua.LNil) + return 1 + } + + if index >= len(list) { + L.Push(lua.LNil) + return 1 + } + + L.Push(lua.LNumber(index + 1)) + L.Push(lWrapFile(L, list[index])) + + return 2 + + })) + + ud := L.NewUserData() + ud.Value = dir.allFiles + + L.Push(ud) + L.Push(lua.LNumber(0)) + + return 3 +} + +// luaDirSortby returns directory's sorting method name. +func luaDirSortby(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LString(dir.sortby)) + return 1 +} + +// getter +func luaDirDircounts(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.dircounts)) + return 1 +} + +// getter +func luaLuaDirfirst(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.dirfirst)) + return 1 +} + +// getter +func luaDirDironly(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.dironly)) + return 1 +} + +// getter +func luaDirHidden(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.hidden)) + return 1 +} + +// getter +func luaDirReverse(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.reverse)) + return 1 +} + +// luaDirVisualAnchor is a getter & setter for anchor position of visual mode +// selection range. +func luaDirVisualAnchor(L *lua.LState) int { + dir := lCheckDir(L, 1) + + if L.GetTop() > 1 { + tryRaiseNonSyncLuaStateError(L) + + value := L.CheckInt(2) + dir.visualAnchor = value + } + + L.Push(lua.LNumber(dir.visualAnchor)) + return 1 +} + +// luaDirVisualWrap is getter and setter for wrapping direction of visual mode. +func luaDirVisualWrap(L *lua.LState) int { + dir := lCheckDir(L, 1) + + if L.GetTop() > 1 { + tryRaiseNonSyncLuaStateError(L) + + value := L.CheckInt(2) + dir.visualWrap = value + } + + L.Push(lua.LNumber(dir.visualWrap)) + return 1 +} + +func luaDirHiddenFiles(L *lua.LState) int { + dir := lCheckDir(L, 1) + hiddenFilesTable := L.NewTable() + for _, file := range dir.hiddenfiles { + hiddenFilesTable.Append(lua.LString(file)) + } + + L.Push(hiddenFilesTable) + + return 1 +} + +func luaDirFilter(L *lua.LState) int { + dir := lCheckDir(L, 1) + filterTable := L.NewTable() + for _, file := range dir.filter { + filterTable.Append(lua.LString(file)) + } + + L.Push(filterTable) + + return 1 +} + +func luaDirFilterLen(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LNumber(len(dir.filter))) + return 1 +} + +func luaDirFilterGetIndex(L *lua.LState) int { + dir := lCheckDir(L, 1) + index := L.CheckInt(2) + if index <= 0 || index > len(dir.filter) { + L.ArgError(2, fmt.Sprintf("index out of range: %d (max index %d)", index, len(dir.filter))) + } + L.Push(lua.LString(dir.filter[index-1])) + return 1 +} + +func luaDirIterFilters(L *lua.LState) int { + dir := lCheckDir(L, 1) + + L.Push(L.NewFunction(func(L *lua.LState) int { + ud := L.CheckUserData(1) + index := L.CheckInt(2) + + list, ok := ud.Value.([]string) + if !ok { + L.Push(lua.LNil) + return 1 + } + + if index >= len(list) { + L.Push(lua.LNil) + return 1 + } + + L.Push(lua.LNumber(index + 1)) + L.Push(lua.LString(list[index])) + + return 2 + })) + + ud := L.NewUserData() + ud.Value = dir.filter + + L.Push(ud) + L.Push(lua.LNumber(0)) + + return 3 +} + +// getter +func luaDirSortignorecase(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.sortignorecase)) + return 1 +} + +// getter +func luaDirSortignoredia(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.sortignoredia)) + return 1 +} + +// luaDirNoPerm returns true if progm doesn't have permission to open this directory. +func luaDirNoPerm(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LBool(dir.noPerm)) + return 1 +} + +// luaDirSort runs sorting for current directory +func luaDirSort(L *lua.LState) int { + dir := lCheckDir(L, 1) + + if msgExpr := getLuaSortingMethod(string(dir.sortby)); msgExpr != nil { + // call sort action directly to avoid potential Lua state dead lock. + err := sortByLuaMsgOnState(L, msgExpr, dir) + if err != nil { + log.Println(err) + } + } else { + dir.sort() + } + + return 0 +} + +func luaDirName(L *lua.LState) int { + dir := lCheckDir(L, 1) + L.Push(lua.LString(dir.name())) + return 1 +} + +// luaDirVisualSelectioins returns a list of path selected in visual mode. +func luaDirVisualSelectioins(L *lua.LState) int { + dir := lCheckDir(L, 1) + tbl := L.NewTable() + + paths := dir.visualSelections() + for _, path := range paths { + tbl.Append(lua.LString(path)) + } + + L.Push(tbl) + + return 1 +} + +// luaDirSel moves cursor to file with given name, and move new cursor position +// into UI window according to given window height. +func luaDirSel(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + dir := lCheckDir(L, 1) + name := L.CheckString(2) + + height := int(L.CheckNumber(3)) + dir.sel(name, height) + + return 0 +} + +// luaDirBoundPos restrict `pos` value of directory to UI window height range, +// and applies `scrolloff` option value. +func luaDirBoundPos(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + dir := lCheckDir(L, 1) + height := L.CheckInt(2) + dir.boundPos(height) + return 0 +} + +// luaDirExtraData can get & stores value to a map associated with this file. +// Only number, string, boolean, nil value are supported. +func luaDirExtraData(L *lua.LState) int { + dir := lCheckDir(L, 1) + key := L.CheckString(2) + + nargs := L.GetTop() + if nargs >= 3 { + tryRaiseNonSyncLuaStateError(L) + value := L.Get(3) + + if dir.extraLuaData == nil { + dir.extraLuaData = make(map[string]any) + } + + if value == lua.LNil { + delete(dir.extraLuaData, key) + return 0 + } + + goValue, err := luaPlainValueToGoValue(value) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + dir.extraLuaData[key] = goValue + L.Push(value) + + return 1 + } + + if dir.extraLuaData == nil { + L.Push(lua.LNil) + return 1 + } + + goValue := dir.extraLuaData[key] + value, err := goValueToLuaValue(L, goValue) + + L.Push(value) + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +func luaDirExtraDataKeys(L *lua.LState) int { + dir := lCheckDir(L, 1) + + tbl := L.NewTable() + for k := range dir.extraLuaData { + tbl.Append(lua.LString(k)) + } + + L.Push(tbl) + + return 1 +} + +// ---------------------------------------------------------------------------- +// Type nav + +const luaNavTypeName = "lf.nav" + +func lRegisterNavType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaNavTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "copy_jobs": luaNavCopyJobs, + "copy_bytes": luaNavCopyBytes, + "copy_total": luaNavCopyTotal, + "move_count": luaNavMoveCount, + "move_total": luaNavMoveTotal, + "get_clipboard": luaNavGetClipboard, + "delete_count": luaNavDeleteCount, + "delete_total": luaNavDeleteTotal, + "get_marks_tbl": luaNavGetMarksTbl, + "get_mark_path": luaNavGetMarkPath, + "get_tag_tbl": luaNavGetTagTbl, + "get_tag": luaNavGetTag, + "height": luaNavHeight, + "get_find_pattern": luaNavGetFindPattern, + "is_find_back": luaNavIsFindBack, + "get_search_pattern": luaNavGetSearchPattern, + "is_search_back": luaNavIsSearchBack, + "get_search_index": luaNavGetSearchInd, + "get_search_pos": luaNavGetSearchPos, + "jump_list": luaNavJumpList, + "jump_list_len": luaNavJumpListLen, + "jump_list_get_index": luaNavJumpGetIndex, + "curr_jump_list_index": luaNavCurrJumpListInd, + + "get_dir": luaNavGetDir, + "add_jump_list": luaNavAddJumpList, + "cd_jump_list_prev": luaNavCdJumpListPrev, + "cd_jump_list_next": luaNavCdJumpListNext, + "renew": luaNavRenew, + "reload": luaNavReload, + "update_position": luaNavUpdatePosition, + "preload": luaNavPreload, + "sort": luaNavSort, + "set_filter": luaNavSetFilter, + "up": luaNavUp, + "down": luaNavDown, + "scroll_up": luaNavScrollUp, + "scroll_down": luaNavScrollDown, + "updir": luaNavUpDir, + "open": luaNavOpen, + "top": luaNavTop, + "bottom": luaNavBottom, + "high": luaNavHigh, + "middle": luaNavMiddle, + "low": luaNavLow, + "move": luaNavMove, + + "select": luaNavSelect, + "toggle_selection": luaNavToggleSelection, + "toggle": luaNavToggle, + "tag_toggle_selection": luaNavTagToggleSelection, + "tag_toggle": luaNavTagToggle, + "tag": luaNavTag, + "invert": luaNavInvert, + "unselect": luaNavUnselect, + "unselect_one": luaNavUnselectOne, + "cd": luaNavCd, + "glob_sel": luaNavGlobSel, + + "find_next": luaNavFindNext, + "find_prev": luaNavFindPrev, + "search_next": luaNavSearchNext, + "search_prev": luaNavSearchPrev, + + "remove_mark": luaNavRemoveMark, + "read_marks": luaNavReadMarks, + "write_marks": luaNavWriteMarks, + "read_tags": luaNavReadTags, + "write_tags": luaNavWriteTags, + + "is_visual_mode": luaNavIsVisualMode, + "curr_dir": luaNavCurrDir, + "curr_file": luaNavCurrFile, + "curr_selections": luaNavCurrSelections, + "curr_file_or_selection": luaNavCurrFileOrSelection, + + "calc_dir_size": luaNavCalcDirSize, + })) + + return mt +} + +func lCheckNav(L *lua.LState, index int) *nav { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*nav); ok { + return v + } + + L.ArgError(index, "value of type `Nav` expected") + + return nil +} + +func lWrapNav(L *lua.LState, data *nav) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaNavTypeName)) + + return ud +} + +func lAddNavToState(L *lua.LState, data *nav) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapNav(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaNavCopyJobs(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.copyJobs)) + return 1 +} + +func luaNavCopyBytes(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.copyBytes)) + return 1 +} + +func luaNavCopyTotal(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.copyTotal)) + return 1 +} + +func luaNavMoveCount(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.moveCount)) + return 1 +} + +func luaNavMoveTotal(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.moveTotal)) + return 1 +} + +func luaNavDeleteCount(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.deleteCount)) + return 1 +} + +func luaNavDeleteTotal(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.deleteTotal)) + return 1 +} + +func luaNavGetClipboard(L *lua.LState) int { + nav := lCheckNav(L, 1) + return lAddClipboardToState(L, &nav.clipboard) +} + +func luaNavGetMarksTbl(L *lua.LState) int { + nav := lCheckNav(L, 1) + + markTbl := L.NewTable() + for mark, path := range nav.marks { + markTbl.RawSetString(mark, lua.LString(path)) + } + + L.Push(markTbl) + + return 1 +} + +func luaNavGetMarkPath(L *lua.LState) int { + nav := lCheckNav(L, 1) + mark := L.CheckString(2) + + path, ok := nav.marks[mark] + if !ok { + return 0 + } + + L.Push(lua.LString(path)) + + return 1 +} + +func luaNavGetTagTbl(L *lua.LState) int { + nav := lCheckNav(L, 1) + + tbl := L.NewTable() + for k, v := range nav.tags { + tbl.RawSetString(k, lua.LString(v)) + } + + L.Push(tbl) + + return 1 +} + +// luaNavGetTag returns tag of given path, returns `nil` when +// no tag is set for target path. +func luaNavGetTag(L *lua.LState) int { + context := lCheckNav(L, 1) + path := L.CheckString(2) + + tag, ok := context.tags[path] + if !ok { + return 0 + } + + L.Push(lua.LString(tag)) + + return 1 +} + +func luaNavHeight(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.height)) + return 1 +} + +func luaNavGetFindPattern(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LString(nav.find)) + return 1 +} + +func luaNavIsFindBack(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.findBack)) + return 1 +} + +func luaNavGetSearchPattern(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LString(nav.search)) + return 1 +} + +func luaNavIsSearchBack(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.searchBack)) + return 1 +} + +func luaNavGetSearchInd(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.searchInd)) + return 1 +} + +func luaNavGetSearchPos(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.searchPos)) + return 1 +} + +func luaNavJumpList(L *lua.LState) int { + nav := lCheckNav(L, 1) + + tbl := L.NewTable() + for _, path := range nav.jumpList { + tbl.Append(lua.LString(path)) + } + + L.Push(tbl) + return 1 +} + +func luaNavJumpListLen(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(len(nav.jumpList))) + return 1 +} + +func luaNavJumpGetIndex(L *lua.LState) int { + nav := lCheckNav(L, 1) + index := L.CheckInt(2) + + if index <= 0 || index > len(nav.jumpList) { + L.ArgError(2, fmt.Sprintf("index out of range: %d (max index %d)", index, len(nav.jumpList))) + } + + L.Push(lua.LString(nav.jumpList[index-1])) + + return 1 +} + +func luaNavCurrJumpListInd(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LNumber(nav.jumpListInd)) + return 1 +} + +func luaNavGetDir(L *lua.LState) int { + nav := lCheckNav(L, 1) + path := L.CheckString(2) + dir := nav.getDir(path) + return lAddDirToState(L, dir) +} + +func luaNavAddJumpList(L *lua.LState) int { + nav := lCheckNav(L, 1) + nav.addJumpList() + return 0 +} + +func luaNavCdJumpListPrev(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.cdJumpListPrev() + + return 0 +} + +func luaNavCdJumpListNext(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.cdJumpListNext() + + return 0 +} + +func luaNavRenew(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.renew() + + return 0 +} + +func luaNavReload(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.reload() + + return 0 +} + +func luaNavUpdatePosition(L *lua.LState) int { + nav := lCheckNav(L, 1) + nav.position() + return 0 +} + +func luaNavPreload(L *lua.LState) int { + nav := lCheckNav(L, 1) + nav.preload() + return 0 +} + +func luaNavSort(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.sort() + + return 0 +} + +func luaNavSetFilter(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + tbl := L.CheckTable(2) + + nPatt := tbl.Len() + if nPatt <= 0 { + return 0 + } + + patterns := make([]string, nPatt) + for i := 1; i <= nPatt; i++ { + value := tbl.RawGetInt(i) + pattern, ok := value.(lua.LString) + if ok { + patterns = append(patterns, string(pattern)) + } + } + + err := nav.setFilter(patterns) + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavUp(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + dist := L.CheckNumber(2) + + moved := nav.up(int(dist)) + L.Push(lua.LBool(moved)) + + return 1 +} + +func luaNavDown(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + dist := L.CheckNumber(2) + + moved := nav.down(int(dist)) + L.Push(lua.LBool(moved)) + + return 1 +} + +func luaNavScrollUp(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + dist := L.CheckNumber(2) + + moved := nav.scrollUp(int(dist)) + L.Push(lua.LBool(moved)) + + return 1 +} + +func luaNavScrollDown(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + dist := L.CheckNumber(2) + + moved := nav.scrollDown(int(dist)) + L.Push(lua.LBool(moved)) + + return 1 +} + +func luaNavUpDir(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + + err := nav.updir() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavOpen(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + err := nav.open() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavTop(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.top())) + + return 1 +} + +func luaNavBottom(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.bottom())) + + return 1 +} + +func luaNavHigh(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.high())) + + return 1 +} + +func luaNavMiddle(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.middle())) + + return 1 +} + +func luaNavLow(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.low())) + + return 1 +} + +func luaNavMove(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + index := L.CheckNumber(2) + + nav.move(int(index)) + + return 0 +} + +func luaNavSelect(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + path := L.CheckString(2) + + nav.selections[path] = nav.selectionInd + nav.selectionInd++ + + return 0 +} + +func luaNavToggleSelection(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + path := L.CheckString(2) + + nav.toggleSelection(path) + + return 0 +} + +func luaNavToggle(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.toggle() + + return 0 +} + +func luaNavTagToggleSelection(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + path := L.CheckString(2) + tag := L.CheckString(3) + + nav.tagToggleSelection(path, tag) + + return 0 +} + +func luaNavTagToggle(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + tag := L.CheckString(2) + + if err := nav.tagToggle(tag); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavTag(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + tag := L.CheckString(2) + + if err := nav.tag(tag); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavInvert(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.invert() + + return 0 +} + +func luaNavUnselect(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + nav.unselect() + + return 0 +} + +func luaNavUnselectOne(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + path := L.CheckString(2) + + if _, ok := nav.selections[path]; ok { + delete(nav.selections, path) + if len(nav.selections) == 0 { + nav.selectionInd = 0 + } + } + + return 0 +} + +func luaNavCd(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + path := L.CheckString(2) + + if err := nav.cd(path); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavGlobSel(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + pattern := L.CheckString(2) + invert := L.CheckBool(3) + + if err := nav.globSel(pattern, invert); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavFindNext(L *lua.LState) int { + nav := lCheckNav(L, 1) + moved, found := nav.findNext() + L.Push(lua.LBool(moved)) + L.Push(lua.LBool(found)) + return 2 +} + +func luaNavFindPrev(L *lua.LState) int { + nav := lCheckNav(L, 1) + moved, found := nav.findPrev() + L.Push(lua.LBool(moved)) + L.Push(lua.LBool(found)) + return 2 +} + +func luaNavSearchNext(L *lua.LState) int { + nav := lCheckNav(L, 1) + moved, err := nav.searchNext() + L.Push(lua.LBool(moved)) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +func luaNavSearchPrev(L *lua.LState) int { + nav := lCheckNav(L, 1) + moved, err := nav.searchPrev() + L.Push(lua.LBool(moved)) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +func luaNavRemoveMark(L *lua.LState) int { + nav := lCheckNav(L, 1) + mark := L.CheckString(2) + + err := nav.removeMark(mark) + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaNavReadMarks(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + err := nav.readMarks() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + return 0 +} + +func luaNavWriteMarks(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + err := nav.writeMarks() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + return 0 +} + +func luaNavReadTags(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + err := nav.readTags() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + return 0 +} + +func luaNavWriteTags(L *lua.LState) int { + tryRaiseNonSyncLuaStateError(L) + + nav := lCheckNav(L, 1) + err := nav.writeTags() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + return 0 +} + +func luaNavIsVisualMode(L *lua.LState) int { + nav := lCheckNav(L, 1) + L.Push(lua.LBool(nav.isVisualMode())) + return 1 +} + +func luaNavCurrDir(L *lua.LState) int { + nav := lCheckNav(L, 1) + return lAddDirToState(L, nav.currDir()) +} + +func luaNavCurrFile(L *lua.LState) int { + nav := lCheckNav(L, 1) + return lAddFileToState(L, nav.currFile()) +} + +func luaNavCurrSelections(L *lua.LState) int { + nav := lCheckNav(L, 1) + + tbl := L.NewTable() + selections := nav.currSelections() + for _, path := range selections { + tbl.Append(lua.LString(path)) + } + + L.Push(tbl) + + return 1 +} + +func luaNavCurrFileOrSelection(L *lua.LState) int { + nav := lCheckNav(L, 1) + + results, err := nav.currFileOrSelections() + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + tbl := L.NewTable() + for _, path := range results { + tbl.Append(lua.LString(path)) + } + + L.Push(tbl) + + return 1 +} + +func luaNavCalcDirSize(L *lua.LState) int { + nav := lCheckNav(L, 1) + err := nav.calcDirSize() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + return 0 +} + +// ---------------------------------------------------------------------------- +// Type ui + +const luaUITypeName = "lf.ui" + +func lRegisterUIType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaUITypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "screen": luaUIScreen, + "wins_len": luaUIWinsLen, + "wins_get_index": luaUIWinsGetIndex, + "iter_wins": luaUIIterWins, + "prompt_win": luaUIPromptWin, + "msg_win": luaUIMsgWin, + "menu_win": luaUIMenuWin, + + "msg": luaUIMsg, + "menu": luaUIMenu, + "cmd_prefix": luaUICmdPrefix, + "key_acc": luaUIKeyAcc, + "key_count": luaUIKeyCount, + + "styles": luaUIStyles, + "icons": luaUIIcons, + + "win_at": luaUIWinAt, + "renew": luaUIRenew, + "echo": luaUIEcho, + "echomsg": luaUIEchoMsg, + "echoerr": luaUIEchhoErr, + "load_file": luaUILoadFile, + })) + + return mt +} + +func lCheckUI(L *lua.LState, index int) *ui { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*ui); ok { + return v + } + + L.ArgError(index, "value of type `UI` expected") + + return nil +} + +func lWrapUI(L *lua.LState, data *ui) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaUITypeName)) + + return ud +} + +func lAddUIToState(L *lua.LState, data *ui) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapUI(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaUIScreen(L *lua.LState) int { + ui := lCheckUI(L, 1) + return lAddTcellScreenToState(L, ui.screen) +} + +func luaUIWinsLen(L *lua.LState) int { + ui := lCheckUI(L, 1) + L.Push(lua.LNumber(len(ui.wins))) + return 1 +} + +func luaUIWinsGetIndex(L *lua.LState) int { + ui := lCheckUI(L, 1) + index := L.CheckInt(2) + + if index <= 0 || index > len(ui.wins) { + L.ArgError(2, fmt.Sprintf("index out of range: %d (max index %d)", index, len(ui.wins))) + } + + return lAddWinToState(L, ui.wins[index-1]) +} + +func luaUIIterWins(L *lua.LState) int { + ui := lCheckUI(L, 1) + + L.Push(L.NewFunction(func(L *lua.LState) int { + ud := L.CheckUserData(1) + index := L.CheckInt(2) + + list, ok := ud.Value.([]*win) + if !ok { + L.Push(lua.LNil) + return 1 + } + + if index >= len(list) { + L.Push(lua.LNil) + return 1 + } + + L.Push(lua.LNumber(index + 1)) + L.Push(lWrapWin(L, list[index])) + + return 2 + })) + + ud := L.NewUserData() + ud.Value = ui.wins + + L.Push(ud) + L.Push(lua.LNumber(0)) + + return 3 +} + +func luaUIPromptWin(L *lua.LState) int { + ui := lCheckUI(L, 1) + return lAddWinToState(L, ui.promptWin) +} + +func luaUIMsgWin(L *lua.LState) int { + ui := lCheckUI(L, 1) + return lAddWinToState(L, ui.msgWin) +} + +func luaUIMenuWin(L *lua.LState) int { + ui := lCheckUI(L, 1) + return lAddWinToState(L, ui.menuWin) +} + +func luaUIMsg(L *lua.LState) int { + ui := lCheckUI(L, 1) + L.Push(lua.LString(ui.msg)) + return 1 +} + +func luaUIMenu(L *lua.LState) int { + ui := lCheckUI(L, 1) + L.Push(lua.LString(ui.menu)) + return 1 +} + +func luaUICmdPrefix(L *lua.LState) int { + ui := lCheckUI(L, 1) + L.Push(lua.LString(ui.cmdPrefix)) + return 1 +} + +func luaUIKeyAcc(L *lua.LState) int { + ui := lCheckUI(L, 1) + L.Push(lua.LString(ui.keyAcc)) + return 1 +} + +func luaUIKeyCount(L *lua.LState) int { + ui := lCheckUI(L, 1) + L.Push(lua.LString(ui.keyCount)) + return 1 +} + +func luaUIStyles(L *lua.LState) int { + ui := lCheckUI(L, 1) + return lAddStyleMapToState(L, &ui.styles) +} + +func luaUIIcons(L *lua.LState) int { + ui := lCheckUI(L, 1) + return lAddIconMapToState(L, &ui.icons) +} + +func luaUIWinAt(L *lua.LState) int { + ui := lCheckUI(L, 1) + x := L.CheckInt(2) + y := L.CheckInt(3) + + index, win := ui.winAt(x, y) + + L.Push(lua.LNumber(index)) + lAddWinToState(L, win) + + return 2 +} + +func luaUIRenew(L *lua.LState) int { + ui := lCheckUI(L, 1) + ui.renew() + return 0 +} + +// luaUIEcho prints content to lf message bar. +func luaUIEcho(L *lua.LState) int { + ui := lCheckUI(L, 1) + + st := 2 + nArgs := L.GetTop() + args := make([]string, nArgs-st+1) + for i := st; i <= nArgs; i++ { + args[i-st] = L.Get(i).String() + } + + ui.exprChan <- &callExpr{"echo", args, 1} + + return 0 +} + +// luaUIEcho prints content to both lf message bar and log. +func luaUIEchoMsg(L *lua.LState) int { + ui := lCheckUI(L, 1) + + st := 2 + nArgs := L.GetTop() + args := make([]string, nArgs-st+1) + for i := st; i <= nArgs; i++ { + args[i-st] = L.Get(i).String() + } + + ui.exprChan <- &callExpr{"echomsg", args, 1} + + return 0 +} + +// luaUIEcho prints error message to both lf message bar and log. +func luaUIEchhoErr(L *lua.LState) int { + ui := lCheckUI(L, 1) + + st := 2 + nArgs := L.GetTop() + args := make([]string, nArgs-st+1) + for i := st; i <= nArgs; i++ { + args[i-st] = L.Get(i).String() + } + + ui.exprChan <- &callExpr{"echoerr", args, 1} + + return 0 +} + +func luaUILoadFile(L *lua.LState) int { + ui := lCheckUI(L, 1) + isVolatile := L.CheckBool(2) + + app, err := getAppObjectFromLuaGlobals(L) + if err != nil { + L.RaiseError("failed to get app object: %s", err) + return 0 + } + + ui.loadFile(app, isVolatile) + + return 0 +} + +// ---------------------------------------------------------------------------- +// type win + +const luaWinTypeName = "lf.win" + +func lRegisterWinType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaWinTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaWinNew, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "w": luaWinW, + "h": luaWinH, + "x": luaWinX, + "y": luaWinY, + + "renew": luaWinRenew, + + "print": luaWinPrint, + "print_line": luaWinPrintLine, + "print_right": luaWinPrintRight, + "print_msg": luaWinPrintMsg, + })) + + return mt +} + +func lCheckWin(L *lua.LState, index int) *win { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*win); ok { + return v + } + + L.ArgError(index, "value of type `Win` expected") + + return nil +} + +func lWrapWin(L *lua.LState, data *win) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaWinTypeName)) + + return ud +} + +func lAddWinToState(L *lua.LState, data *win) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapWin(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaWinNew(L *lua.LState) int { + w := L.CheckInt(1) + h := L.CheckInt(1) + x := L.CheckInt(1) + y := L.CheckInt(1) + + return lAddWinToState(L, newWin(w, h, x, y)) +} + +// ---------------------------------------------------------------------------- + +func luaWinW(L *lua.LState) int { + win := lCheckWin(L, 1) + L.Push(lua.LNumber(win.w)) + return 1 +} + +func luaWinH(L *lua.LState) int { + win := lCheckWin(L, 1) + L.Push(lua.LNumber(win.h)) + return 1 +} + +func luaWinX(L *lua.LState) int { + win := lCheckWin(L, 1) + L.Push(lua.LNumber(win.x)) + return 1 +} + +func luaWinY(L *lua.LState) int { + win := lCheckWin(L, 1) + L.Push(lua.LNumber(win.y)) + return 1 +} + +func luaWinRenew(L *lua.LState) int { + win := lCheckWin(L, 1) + w := L.CheckInt(1) + h := L.CheckInt(1) + x := L.CheckInt(1) + y := L.CheckInt(1) + + win.renew(w, h, x, y) + + return 0 +} + +func luaWinPrint(L *lua.LState) int { + win := lCheckWin(L, 1) + screen := lCheckTcellScreen(L, 2) + x := L.CheckInt(3) + y := L.CheckInt(4) + st := lCheckTcellStyle(L, 5) + str := L.CheckString(6) + + result := win.print(screen, x, y, *st, str) + + return lAddTcellStyleToState(L, &result) +} + +// luaWinPrintLine prints content to screen, and fills the gap between text end +// and window's edge with whitespace. +func luaWinPrintLine(L *lua.LState) int { + win := lCheckWin(L, 1) + screen := lCheckTcellScreen(L, 2) + x := L.CheckInt(3) + y := L.CheckInt(4) + st := lCheckTcellStyle(L, 5) + str := L.CheckString(6) + + win.printLine(screen, x, y, *st, str) + + return 0 +} + +// luaWinPrintRight prints right aligned text. +func luaWinPrintRight(L *lua.LState) int { + win := lCheckWin(L, 1) + screen := lCheckTcellScreen(L, 2) + y := L.CheckInt(4) + st := lCheckTcellStyle(L, 5) + str := L.CheckString(6) + + win.printRight(screen, y, *st, str) + + return 0 +} + +// luaWinPrintMsg prints text with reversed style (exchanging foreground and +// background color) to screen. +func luaWinPrintMsg(L *lua.LState) int { + win := lCheckWin(L, 1) + screen := lCheckTcellScreen(L, 2) + msg := L.CheckString(3) + + win.printMsg(screen, msg) + + return 0 +} + +// ---------------------------------------------------------------------------- +// type dirStyle + +const luaDirStyleTypeName = "lf.dirStyle" + +func lRegisterDirStyleType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaDirStyleTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "colors": luaDirStyleColors, + "icons": luaDirStyleIcons, + "role": luaDirStyleRole, + })) + + return mt +} + +func lCheckDirStyle(L *lua.LState, index int) *dirStyle { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*dirStyle); ok { + return v + } + + L.ArgError(index, "value of type `DirStyle` expected") + + return nil +} + +func lWrapDirStyle(L *lua.LState, data *dirStyle) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaDirStyleTypeName)) + + return ud +} + +func lAddDirStyleToState(L *lua.LState, data *dirStyle) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapDirStyle(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaDirStyleColors(L *lua.LState) int { + dirSt := lCheckDirStyle(L, 1) + return lAddStyleMapToState(L, &dirSt.colors) +} + +func luaDirStyleIcons(L *lua.LState) int { + dirSt := lCheckDirStyle(L, 1) + return lAddIconMapToState(L, &dirSt.icons) +} + +func luaDirStyleRole(L *lua.LState) int { + dirSt := lCheckDirStyle(L, 1) + L.Push(lua.LNumber(dirSt.role)) + return 1 +} + +// ---------------------------------------------------------------------------- +// type styleMap + +const luaStyleMapTypeName = "lf.styleMap" + +func lRegisterStyleMapType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaStyleMapTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "get": luaStyleMapGet, + })) + + return mt +} + +func lCheckStyleMap(L *lua.LState, index int) *styleMap { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*styleMap); ok { + return v + } + + L.ArgError(index, "value of type `StyleMap` expected") + + return nil +} + +func lWrapStyleMap(L *lua.LState, data *styleMap) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaStyleMapTypeName)) + + return ud +} + +func lAddStyleMapToState(L *lua.LState, data *styleMap) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapStyleMap(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaStyleMapGet(L *lua.LState) int { + stMap := lCheckStyleMap(L, 1) + file := lCheckFile(L, 2) + st := stMap.get(file) + return lAddTcellStyleToState(L, &st) +} + +// ---------------------------------------------------------------------------- +// type iconDef + +const luaIconDefTypeName = "lf.iconDef" + +func lRegisterIconDefType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaIconDefTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "icon": luaIconDefIcon, + "has_style": luaIconDefHasStyle, + "style": luaIconDefStyle, + })) + + return mt +} + +func lCheckIconDef(L *lua.LState, index int) *iconDef { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*iconDef); ok { + return v + } + + L.ArgError(index, "value of type `IconDef` expected") + + return nil +} + +func lWrapIconDef(L *lua.LState, data *iconDef) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaIconDefTypeName)) + + return ud +} + +func lAddIconDefToState(L *lua.LState, data *iconDef) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapIconDef(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaIconDefIcon gets icon string of a file. +func luaIconDefIcon(L *lua.LState) int { + def := lCheckIconDef(L, 1) + L.Push(lua.LString(def.icon)) + return 1 +} + +// luaIconDefHasStyle returns if this icon has style. +func luaIconDefHasStyle(L *lua.LState) int { + def := lCheckIconDef(L, 1) + L.Push(lua.LBool(def.hasStyle)) + return 1 +} + +// luaIconDefStyle returns style object binded with this icon. +func luaIconDefStyle(L *lua.LState) int { + def := lCheckIconDef(L, 1) + return lAddTcellStyleToState(L, &def.style) +} + +// ---------------------------------------------------------------------------- +// type iconMap + +const luaIconMapTypeName = "lf.iconMap" + +func lRegisterIconMapType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaIconMapTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "get": luaIconMapGet, + })) + + return mt +} + +func lCheckIconMap(L *lua.LState, index int) *iconMap { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*iconMap); ok { + return v + } + + L.ArgError(index, "value of type `IconMap` expected") + + return nil +} + +func lWrapIconMap(L *lua.LState, data *iconMap) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaIconMapTypeName)) + + return ud +} + +func lAddIconMapToState(L *lua.LState, data *iconMap) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapIconMap(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaIconMapGet gets icon definition of a file. +func luaIconMapGet(L *lua.LState) int { + im := lCheckIconMap(L, 1) + file := lCheckFile(L, 2) + def := im.get(file) + return lAddIconDefToState(L, &def) +} + +// ---------------------------------------------------------------------------- +// type dirContext + +const luaDirContextTypeName = "lf.dirContext" + +func lRegisterDirContextType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaDirContextTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaDirContextNew, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "selections": luaDirContextSelections, + "clipboard": luaDirContextClipboard, + "tags": luaDirContextTags, + + "get_selection_index": luaDirContextGetSelectionIndex, + "get_tag": luaDirContextGetTag, + })) + + return mt +} + +func lCheckDirContext(L *lua.LState, index int) *dirContext { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*dirContext); ok { + return v + } + + L.ArgError(index, "value of type `DirContext` expected") + + return nil +} + +func lWrapDirContext(L *lua.LState, data *dirContext) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaDirContextTypeName)) + + return ud +} + +func lAddDirContextToState(L *lua.LState, data *dirContext) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapDirContext(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaDirContextNew(L *lua.LState) int { + tbl := L.CheckTable(1) + + selectionsTbl, ok := tbl.RawGetString("selections").(*lua.LTable) + if !ok { + L.RaiseError("key `selections` should be a table") + } + selections := map[string]int{} + selectionsTbl.ForEach(func(kValue, vValue lua.LValue) { + key, keyOk := kValue.(lua.LString) + value, valueOk := vValue.(lua.LNumber) + if keyOk && valueOk { + selections[string(key)] = int(value) + } + }) + + clipboardValue, ok := tbl.RawGetString("clipboard").(*lua.LUserData) + if !ok { + L.RaiseError("key `clipboard` should be a userdata") + } + clipboard, ok := clipboardValue.Value.(*clipboard) + if !ok { + L.RaiseError("key `clipboard` should be a clipboard object") + } + + tagTbl, ok := tbl.RawGetString("tags").(*lua.LTable) + if !ok { + L.RaiseError("key `tags` should be a table") + } + tags := map[string]string{} + tagTbl.ForEach(func(kValue, vValue lua.LValue) { + key, keyOk := kValue.(lua.LString) + value, valueOk := vValue.(lua.LString) + if keyOk && valueOk { + tags[string(key)] = string(value) + } + }) + + context := &dirContext{ + selections: selections, + clipboard: *clipboard, + tags: tags, + } + + return lAddDirContextToState(L, context) +} + +// ---------------------------------------------------------------------------- + +func luaDirContextSelections(L *lua.LState) int { + context := lCheckDirContext(L, 1) + + tbl := L.NewTable() + for k, v := range context.selections { + tbl.RawSetString(k, lua.LNumber(v)) + } + + L.Push(tbl) + + return 1 +} + +func luaDirContextClipboard(L *lua.LState) int { + context := lCheckDirContext(L, 1) + return lAddClipboardToState(L, &context.clipboard) +} + +func luaDirContextTags(L *lua.LState) int { + context := lCheckDirContext(L, 1) + + tbl := L.NewTable() + for k, v := range context.tags { + tbl.RawSetString(k, lua.LString(v)) + } + + L.Push(tbl) + + return 1 +} + +// luaDirContextGetSelectionIndex returns 1-based selection index of +// given path, returns 0 when that path is not selected. +func luaDirContextGetSelectionIndex(L *lua.LState) int { + context := lCheckDirContext(L, 1) + path := L.CheckString(2) + + index, found := context.selections[path] + if found { + L.Push(lua.LNumber(index + 1)) + } else { + L.Push(lua.LNumber(0)) + } + + return 1 +} + +// luaDirContextGetTag returns tag of given path, returns `nil` when +// no tag is set for target path. +func luaDirContextGetTag(L *lua.LState) int { + context := lCheckDirContext(L, 1) + path := L.CheckString(2) + + tag, ok := context.tags[path] + if !ok { + return 0 + } + + L.Push(lua.LString(tag)) + + return 1 +} + +// ---------------------------------------------------------------------------- +// type printDirEntryContext + +const luaPrintDirEntryContextTypeName = "lf.printDirEntryContext" + +func lRegisterPrintDirEntryContextType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaPrintDirEntryContextTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaPrintDirEntryContextNew, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "dir": luaPrintDirEntryContextDir, + "dir_beg": luaPrintDirEntryContextDirBeg, + "dir_end": luaPrintDirEntryContextDirEnd, + "dir_style": luaPrintDirEntryContextDirStyle, + + "lnwidth": luaPrintDirEntryContextLnwidth, + "user_width": luaPrintDirEntryContextUserWidth, + "group_width": luaPrintDirEntryContextGroupWidth, + "custom_width": luaPrintDirEntryContextCustomWidth, + + "selections": luaPrintDirEntryContextSelections, + "clipboard": luaPrintDirEntryContextClipboard, + "tags": luaPrintDirEntryContextTags, + "visual_selectioins": luaPrintDirEntryContextVisualSelections, + + "get_selection_index": luaPrintDirEntryContextGetSelectionIndex, + "visual_selection_contain": luaPrintDirEntryContextVisualSelectionsContain, + "get_tag": luaPrintDirEntryContextGetTag, + })) + + return mt +} + +func lCheckPrintDirEntryContext(L *lua.LState, index int) *printDirEntryContext { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*printDirEntryContext); ok { + return v + } + + L.ArgError(index, "value of type `PrintDirEntryContext` expected") + + return nil +} + +func lWrapPrintDirEntryContext(L *lua.LState, data *printDirEntryContext) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaPrintDirEntryContextTypeName)) + + return ud +} + +func lAddPrintDirEntryContextToState(L *lua.LState, data *printDirEntryContext) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapPrintDirEntryContext(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaPrintDirEntryContextNew(L *lua.LState) int { + tbl := L.CheckTable(1) + + dirUd, ok := tbl.RawGetString("dir").(*lua.LUserData) + if !ok { + L.RaiseError("key `dir` should be userdata") + } + dir, ok := dirUd.Value.(*dir) + if !ok { + L.RaiseError("key `dir` should be a dir object") + } + + dirBegValue, ok := tbl.RawGetString("dir_beg").(lua.LNumber) + if !ok { + L.RaiseError("key `dir_beg` should be a number") + } + dirBeg := int(dirBegValue) + + dirEndValue, ok := tbl.RawGetString("dir_end").(lua.LNumber) + if !ok { + L.RaiseError("key `dir_end` should be a number") + } + dirEnd := int(dirEndValue) + + dirStyleUd, ok := tbl.RawGetString("dir_style").(*lua.LUserData) + if !ok { + L.RaiseError("key `dir_style` should be userdata") + } + dirStyle, ok := dirStyleUd.Value.(*dirStyle) + if !ok { + L.RaiseError("key `dir_style should be dirStyle object") + } + + lnwidthValue, ok := tbl.RawGetString("lnwidth").(lua.LNumber) + if !ok { + L.RaiseError("key `lnwidth` should be a number") + } + lnwidth := int(lnwidthValue) + + userWidthValue, ok := tbl.RawGetString("user_width").(lua.LNumber) + if !ok { + L.RaiseError("key `lnwidth` should be a number") + } + userWidth := int(userWidthValue) + + groupWidthValue, ok := tbl.RawGetString("group_width").(lua.LNumber) + if !ok { + L.RaiseError("key `lnwidth` should be a number") + } + groupWidth := int(groupWidthValue) + + customWidthValue, ok := tbl.RawGetString("custom_width").(lua.LNumber) + if !ok { + L.RaiseError("key `lnwidth` should be a number") + } + customWidth := int(customWidthValue) + + selectionsTbl, ok := tbl.RawGetString("selections").(*lua.LTable) + if !ok { + L.RaiseError("key `selections` should be a table") + } + selections := map[string]int{} + selectionsTbl.ForEach(func(kValue, vValue lua.LValue) { + key, keyOk := kValue.(lua.LString) + value, valueOk := vValue.(lua.LNumber) + if keyOk && valueOk { + selections[string(key)] = int(value) + } + }) + + clipboardValue, ok := tbl.RawGetString("clipboard").(*lua.LUserData) + if !ok { + L.RaiseError("key `clipboard` should be a userdata") + } + clipboard, ok := clipboardValue.Value.(*clipboard) + if !ok { + L.RaiseError("key `clipboard` should be a clipboard object") + } + + tagTbl, ok := tbl.RawGetString("tags").(*lua.LTable) + if !ok { + L.RaiseError("key `tags` should be a table") + } + tags := map[string]string{} + tagTbl.ForEach(func(kValue, vValue lua.LValue) { + key, keyOk := kValue.(lua.LString) + value, valueOk := vValue.(lua.LString) + if keyOk && valueOk { + tags[string(key)] = string(value) + } + }) + + visualSelectionTbl, ok := tbl.RawGetString("visual_selections").(*lua.LTable) + if !ok { + L.RaiseError("key `visual_selections` should be a table") + } + visualSelections := []string{} + nVisualSelection := visualSelectionTbl.Len() + for i := 1; i <= nVisualSelection; i++ { + value := visualSelectionTbl.RawGetInt(i) + if path, ok := value.(lua.LString); ok { + visualSelections = append(visualSelections, string(path)) + } + } + + context := &printDirEntryContext{ + dir: dir, + dirBeg: dirBeg, + dirEnd: dirEnd, + dirStyle: dirStyle, + + lnwidth: lnwidth, + userWidth: userWidth, + groupWidth: groupWidth, + customWidth: customWidth, + + selections: selections, + clipboard: *clipboard, + tags: tags, + visualSelections: visualSelections, + } + + return lAddPrintDirEntryContextToState(L, context) +} + +// ---------------------------------------------------------------------------- + +func luaPrintDirEntryContextDir(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + return lAddDirToState(L, context.dir) +} + +func luaPrintDirEntryContextDirBeg(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + L.Push(lua.LNumber(context.dirBeg)) + return 1 +} + +func luaPrintDirEntryContextDirEnd(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + L.Push(lua.LNumber(context.dirEnd)) + return 1 +} + +func luaPrintDirEntryContextDirStyle(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + return lAddDirStyleToState(L, context.dirStyle) +} + +func luaPrintDirEntryContextLnwidth(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + L.Push(lua.LNumber(context.lnwidth)) + return 1 +} + +func luaPrintDirEntryContextUserWidth(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + L.Push(lua.LNumber(context.userWidth)) + return 1 +} + +func luaPrintDirEntryContextGroupWidth(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + L.Push(lua.LNumber(context.groupWidth)) + return 1 +} + +func luaPrintDirEntryContextCustomWidth(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + L.Push(lua.LNumber(context.customWidth)) + return 1 +} + +func luaPrintDirEntryContextSelections(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + + tbl := L.NewTable() + for k, v := range context.selections { + tbl.RawSetString(k, lua.LNumber(v)) + } + + L.Push(tbl) + + return 1 +} + +func luaPrintDirEntryContextClipboard(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + return lAddClipboardToState(L, &context.clipboard) +} + +func luaPrintDirEntryContextTags(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + + tbl := L.NewTable() + for k, v := range context.tags { + tbl.RawSetString(k, lua.LString(v)) + } + + L.Push(tbl) + + return 1 +} + +func luaPrintDirEntryContextVisualSelections(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + + tbl := L.NewTable() + for _, v := range context.visualSelections { + tbl.Append(lua.LString(v)) + } + + L.Push(tbl) + + return 1 +} + +// luaPrintDirEntryContextGetSelectionIndex returns 1-based selection index of +// given path, returns 0 when that path is not selected. +func luaPrintDirEntryContextGetSelectionIndex(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + path := L.CheckString(2) + + index, found := context.selections[path] + if found { + L.Push(lua.LNumber(index + 1)) + } else { + L.Push(lua.LNumber(0)) + } + + return 1 +} + +// luaPrintDirEntryContextVisualSelectionsContain checks if visual selection +// contains given path. +func luaPrintDirEntryContextVisualSelectionsContain(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + path := L.CheckString(2) + + found := slices.Contains(context.visualSelections, path) + L.Push(lua.LBool(found)) + + return 1 +} + +// luaPrintDirEntryContextGetTag returns tag of given path, returns `nil` when +// no tag is set for target path. +func luaPrintDirEntryContextGetTag(L *lua.LState) int { + context := lCheckPrintDirEntryContext(L, 1) + path := L.CheckString(2) + + tag, ok := context.tags[path] + if ok { + L.Push(lua.LString(tag)) + } else { + L.Push(lua.LNil) + } + + return 1 +} + +// ---------------------------------------------------------------------------- +// type clipboard + +const luaClipboardTypeName = "lf.clipboard" + +func lRegisterClipboardType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaClipboardTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "paths": luaClipboardPaths, + "paths_len": luaClipboardPathsLen, + "paths_get_index": luaClipboardPathsGetIndex, + "mode": luaClipboardMode, + "iter_paths": luaClipboardIterPaths, + "contains_path": luaClipboardPathsContain, + })) + + return mt +} + +func lCheckClipboard(L *lua.LState, index int) *clipboard { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*clipboard); ok { + return v + } + + L.ArgError(index, "value of type `Clipboard` expected") + + return nil +} + +func lWrapClipboard(L *lua.LState, data *clipboard) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaClipboardTypeName)) + + return ud +} + +func lAddClipboardToState(L *lua.LState, data *clipboard) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapClipboard(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaClipboardPaths(L *lua.LState) int { + board := lCheckClipboard(L, 1) + + tbl := L.NewTable() + for _, path := range board.paths { + tbl.Append(lua.LString(path)) + } + + L.Push(tbl) + + return 1 +} + +func luaClipboardPathsLen(L *lua.LState) int { + board := lCheckClipboard(L, 1) + L.Push(lua.LNumber(len(board.paths))) + return 1 +} + +func luaClipboardPathsGetIndex(L *lua.LState) int { + board := lCheckClipboard(L, 1) + index := L.CheckInt(2) + if index <= 0 || index > len(board.paths) { + L.ArgError(2, fmt.Sprintf("index out of range: %d (max index %d)", index, len(board.paths))) + } + L.Push(lua.LString(board.paths[index-1])) + return 1 +} + +func luaClipboardMode(L *lua.LState) int { + board := lCheckClipboard(L, 1) + L.Push(lua.LNumber(board.mode)) + return 1 +} + +func luaClipboardIterPaths(L *lua.LState) int { + board := lCheckClipboard(L, 1) + + L.Push(L.NewFunction(func(L *lua.LState) int { + ud := L.CheckUserData(1) + index := L.CheckInt(2) + + list, ok := ud.Value.([]string) + if !ok { + L.Push(lua.LNil) + return 1 + } + + if index >= len(list) { + L.Push(lua.LNil) + return 1 + } + + L.Push(lua.LNumber(index + 1)) + L.Push(lua.LString(list[index])) + + return 2 + })) + + ud := L.NewUserData() + ud.Value = board.paths + + L.Push(ud) + L.Push(lua.LNumber(0)) + + return 3 +} + +func luaClipboardPathsContain(L *lua.LState) int { + board := lCheckClipboard(L, 1) + path := L.CheckString(2) + found := slices.Contains(board.paths, path) + L.Push(lua.LBool(found)) + return 1 +} + +// ---------------------------------------------------------------------------- +// type luaMsgExpr + +const luaLuaMsgExprTypeName = "lf.luaMsgExpr" + +func lRegisterLuaMsgExprType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaLuaMsgExprTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{})) + + return mt +} + +func lCheckLuaMsgExpr(L *lua.LState, index int) *luaMsgExpr { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*luaMsgExpr); ok { + return v + } + + L.ArgError(index, "value of type `LuaMsgExpr` expected") + + return nil +} + +func lWrapLuaMsgExpr(L *lua.LState, data *luaMsgExpr) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaLuaMsgExprTypeName)) + + return ud +} + +func lAddLuaMsgExprToState(L *lua.LState, data *luaMsgExpr) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapLuaMsgExpr(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- +// type luaFuncWriter + +const luaFuncWriterTypeName = "lf.FuncWriter" + +func lRegisterFuncWriterType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaFuncWriterTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaFuncWriterNew, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "write": luaFuncWriterWrite, + })) + + return mt +} + +func lCheckFuncWriter(L *lua.LState, index int) *luaFuncWriter { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*luaFuncWriter); ok { + return v + } + + L.ArgError(index, "value of type `FuncWriter` expected") + + return nil +} + +func lWrapFuncWriter(L *lua.LState, data *luaFuncWriter) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaFuncWriterTypeName)) + + return ud +} + +func lAddFuncWriterToState(L *lua.LState, data *luaFuncWriter) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapFuncWriter(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaFuncWriterNew(L *lua.LState) int { + fn := L.CheckFunction(1) + writer := &luaFuncWriter{ + luaState: L, + fn: fn, + } + return lAddFuncWriterToState(L, writer) +} + +// ---------------------------------------------------------------------------- + +func luaFuncWriterWrite(L *lua.LState) int { + writer := lCheckFuncWriter(L, 1) + content := L.CheckString(2) + + n, err := writer.Write([]byte(content)) + L.Push(lua.LNumber(n)) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +// ---------------------------------------------------------------------------- +// type luaDataStore + +const luaLuaDataStoreTypeName = "lf.luaDataStore" + +func lRegisterLuaDataStoreType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaLuaDataStoreTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "get": luaLuaDataStoreGet, + "set": luaLuaDataStoreSet, + "clear": luaLuaDataStoreClear, + "keys": luaLuaDataStoreKeys, + })) + + return mt +} + +func lCheckLuaDataStore(L *lua.LState, index int) *luaDataStore { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*luaDataStore); ok { + return v + } + + L.ArgError(index, "value of type `LuaDataStore` expected") + + return nil +} + +func lWrapLuaDataStore(L *lua.LState, data *luaDataStore) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaLuaDataStoreTypeName)) + + return ud +} + +/* func lAddLuaDataStoreToState(L *lua.LState, data *luaDataStore) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapLuaDataStore(L, data) + L.Push(ud) + + return 1 +} */ + +// ---------------------------------------------------------------------------- + +func luaLuaDataStoreGet(L *lua.LState) int { + store := lCheckLuaDataStore(L, 1) + key := L.CheckString(2) + + value, err := store.get(L, key) + L.Push(value) + + if err != nil { + L.Push(lua.LString(err.Error())) + return 2 + } + + return 1 +} + +func luaLuaDataStoreSet(L *lua.LState) int { + store := lCheckLuaDataStore(L, 1) + key := L.CheckString(2) + value := L.CheckAny(3) + + err := store.set(key, value) + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaLuaDataStoreClear(L *lua.LState) int { + store := lCheckLuaDataStore(L, 1) + store.clear() + return 0 +} + +func luaLuaDataStoreKeys(L *lua.LState) int { + store := lCheckLuaDataStore(L, 1) + tbl := store.keysAsLuaTbl(L) + L.Push(tbl) + return 1 +} diff --git a/lua_binding_tcell.go b/lua_binding_tcell.go new file mode 100644 index 00000000..81a31243 --- /dev/null +++ b/lua_binding_tcell.go @@ -0,0 +1,497 @@ +package main + +import ( + "strings" + + "github.com/gdamore/tcell/v3" + lua "github.com/yuin/gopher-lua" +) + +// ---------------------------------------------------------------------------- +// type tcell.Style + +const luaTcellStyleTypeName = "tcell.Style" + +func lRegisterTcellStyleType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaTcellStyleTypeName) + + addTcellStyleConstantToMt(mt) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaTcellStyleNew, + "reset_string": luaTcellStyleRestString, + "__tostring": luaTcellStyleMetaTostring, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "tostring": luaTcellStyleTostring, + "wrap": luaTcellStyleWrap, + + "foreground": luaTcellStyleForeground, + "background": luaTcellStyleBackground, + "foreground_rgb": luaTcellStyleForegroundRGB, + "background_rgb": luaTcellStyleBackgroundRGB, + "foreground_name": luaTcellStyleForegroundName, + "background_name": luaTcellStyleBackgroundName, + "foreground_palette": luaTcellStyleForegroundPalette, + "background_palette": luaTcellStyleBackgroundPalette, + + "normal": luaTcellStyleNormal, + "bold": luaTcellStyleBold, + "blink": luaTcellStyleBlink, + "dim": luaTcellStyleDim, + "italic": luaTcellStyleItalic, + "reverse": luaTcellStyleReverse, + "strike_through": luaTcellStyleStrikeThrough, + "underline": luaTcellStyleUnderline, + "set_underline_style": luaTcellStyleSetUnderlineStyle, + "set_underline_color": luaTcellStyleSetUnderlineColor, + + "has_bold": luaTcellStyleHasBold, + "has_blink": luaTcellStyleHasBlink, + "has_reverse": luaTcellStyleHasReverse, + "has_italic": luaTcellStyleHasItalic, + "has_dim": luaTcellStyleHasDim, + "has_strike_through": luaTcellStyleHasStrikeThrough, + "has_underline": luaTcellStyleHasUnderline, + })) + + return mt +} + +func addTcellStyleConstantToMt(mt *lua.LTable) { + mt.RawSetString("UnderlineStyleNone", lua.LNumber(tcell.UnderlineStyleNone)) + mt.RawSetString("UnderlineStyleSolid", lua.LNumber(tcell.UnderlineStyleSolid)) + mt.RawSetString("UnderlineStyleDouble", lua.LNumber(tcell.UnderlineStyleDouble)) + mt.RawSetString("UnderlineStyleCurly", lua.LNumber(tcell.UnderlineStyleCurly)) + mt.RawSetString("UnderlineStyleDotted", lua.LNumber(tcell.UnderlineStyleDotted)) + mt.RawSetString("UnderlineStyleDashed", lua.LNumber(tcell.UnderlineStyleDashed)) +} + +func lCheckTcellStyle(L *lua.LState, index int) *tcell.Style { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*tcell.Style); ok { + return v + } + + L.ArgError(index, "value of type `TcellStyle` expected") + + return nil +} + +func lWrapTcellStyle(L *lua.LState, data *tcell.Style) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaTcellStyleTypeName)) + + return ud +} + +func lAddTcellStyleToState(L *lua.LState, data *tcell.Style) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapTcellStyle(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaTcellStyleNew(L *lua.LState) int { + st := tcell.StyleDefault + return lAddTcellStyleToState(L, &st) +} + +// luaTcellStyleRestString returns reset CSI string. +func luaTcellStyleRestString(L *lua.LState) int { + L.Push(lua.LString("\033[0m")) + return 1 +} + +func luaTcellStyleMetaTostring(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LString(tcellStyleToString(*st))) + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaTcellStyleTostring converts current style to CSI string. Does the same thing +// as __tostring meta method. +func luaTcellStyleTostring(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LString(tcellStyleToString(*st))) + return 1 +} + +// luaTcellStyleWrap takes a list of content strings, and wrap them with CSI string +// form of current style and reset CSI sequens. Result is returned as a single +// string. +func luaTcellStyleWrap(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + + nArgs := L.GetTop() + contents := make([]string, nArgs+1) + + contents[0] = tcellStyleToString(*st) + for i := 2; i <= nArgs; i++ { + contents[i-1] = L.CheckString(i) + } + contents[nArgs] = "\033[0m" + + L.Push(lua.LString(strings.Join(contents, ""))) + return 1 +} + +// luaTcellStyleForeground sets foreground color. +func luaTcellStyleForeground(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + color := lCheckTcellColor(L, 2) + *st = st.Foreground(*color) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleBackground sets background color. +func luaTcellStyleBackground(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + color := lCheckTcellColor(L, 2) + *st = st.Background(*color) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleForegroundRGB sets foreground color with RGB channel value. +func luaTcellStyleForegroundRGB(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + r := L.CheckInt(2) + g := L.CheckInt(3) + b := L.CheckInt(4) + + *st = st.Foreground(tcell.NewRGBColor(int32(r), int32(g), int32(b))) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleBackgroundRGB sets background color with RGB channel value. +func luaTcellStyleBackgroundRGB(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + r := L.CheckInt(2) + g := L.CheckInt(3) + b := L.CheckInt(4) + + *st = st.Background(tcell.NewRGBColor(int32(r), int32(g), int32(b))) + + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleForegroundName sets foreground color with color name or hex code +// starting with `#`. +func luaTcellStyleForegroundName(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + name := L.CheckString(2) + + *st = st.Foreground(tcell.GetColor(name)) + + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleBackgroundName sets background color with color name or hex code +// starting with `#`. +func luaTcellStyleBackgroundName(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + name := L.CheckString(2) + + *st = st.Background(tcell.GetColor(name)) + + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleForegroundPalette sets foreground color with palette index. +func luaTcellStyleForegroundPalette(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + index := L.CheckInt(2) + + *st = st.Foreground(tcell.PaletteColor(index)) + + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleBackgroundPalette sets background color with palette index. +func luaTcellStyleBackgroundPalette(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + index := L.CheckInt(2) + + *st = st.Background(tcell.PaletteColor(index)) + + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleNormal returns the style with all attributes disabled. +// Colors and hyperlinks are preserved +func luaTcellStyleNormal(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + *st = st.Normal() + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleBold enables or disables bold attribute. +func luaTcellStyleBold(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + isActive := L.CheckBool(2) + *st = st.Bold(isActive) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleBlink enables or disables blink attribute. +func luaTcellStyleBlink(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + isActive := L.CheckBool(2) + *st = st.Blink(isActive) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleDim enables or disables dim attribute. +func luaTcellStyleDim(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + isActive := L.CheckBool(2) + *st = st.Dim(isActive) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleItalic enables or disables italic attribute. +func luaTcellStyleItalic(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + isActive := L.CheckBool(2) + *st = st.Italic(isActive) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleReverse enables or disables foreground-background reverse attribute. +func luaTcellStyleReverse(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + isActive := L.CheckBool(2) + *st = st.Reverse(isActive) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleStrikeThrough enables or disables strike-through attribute. +func luaTcellStyleStrikeThrough(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + isActive := L.CheckBool(2) + *st = st.StrikeThrough(isActive) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleUnderline enables or disables underline attribute. +func luaTcellStyleUnderline(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + isActive := L.CheckBool(2) + *st = st.Underline(isActive) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleSetUnderlineStyle sets underline style type. Style type value +// can be found as constant filed in metatable of Style. +// ```lua +// local Style = lf_type.TcellStyle +// print(Style.UnderlineStyleSolid) +// ``` +func luaTcellStyleSetUnderlineStyle(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + ulStyle := L.CheckInt(2) + *st = st.Underline(tcell.UnderlineStyle(ulStyle)) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleSetUnderlineColor sets color of underline. +func luaTcellStyleSetUnderlineColor(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + color := lCheckTcellColor(L, 2) + *st = st.Underline(color) + return lAddTcellStyleToState(L, st) +} + +// luaTcellStyleHasBold checks if current sytle has bold attribute. +func luaTcellStyleHasBold(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LBool(st.HasBold())) + return 1 +} + +// luaTcellStyleHasBold checks if current sytle has blink attribute. +func luaTcellStyleHasBlink(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LBool(st.HasBlink())) + return 1 +} + +// luaTcellStyleHasReverse checks if current sytle has reverse attribute. +func luaTcellStyleHasReverse(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LBool(st.HasReverse())) + return 1 +} + +// luaTcellStyleHasItalic checks if current sytle has italic attribute. +func luaTcellStyleHasItalic(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LBool(st.HasItalic())) + return 1 +} + +// luaTcellStyleHasDim checks if current sytle has dim attribute. +func luaTcellStyleHasDim(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LBool(st.HasDim())) + return 1 +} + +// luaTcellStyleHasStrikeThrough checks if current sytle has strike-through attribute. +func luaTcellStyleHasStrikeThrough(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LBool(st.HasStrikeThrough())) + return 1 +} + +// luaTcellStyleHasUnderline checks if current sytle has underline attribute. +func luaTcellStyleHasUnderline(L *lua.LState) int { + st := lCheckTcellStyle(L, 1) + L.Push(lua.LBool(st.HasUnderline())) + return 1 +} + +// ---------------------------------------------------------------------------- +// type tcell.Color + +const luaTcellColorTypeName = "tcell.Color" + +func lRegisterTcellColorType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaTcellColorTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new_rgb": luaTcellColorNewRgb, + "new_hex": luaTcellColorNewHex, + "new_name": luaTcellColorNewName, + "new_palette": luaTcellColorNewPalette, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{})) + + return mt +} + +func lCheckTcellColor(L *lua.LState, index int) *tcell.Color { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*tcell.Color); ok { + return v + } + + L.ArgError(index, "value of type `TcellColor` expected") + + return nil +} + +func lWrapTcellColor(L *lua.LState, data *tcell.Color) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaTcellColorTypeName)) + + return ud +} + +func lAddTcellColorToState(L *lua.LState, data *tcell.Color) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapTcellColor(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaTcellColorNewRgb creates color userdata with RGB channel value. +func luaTcellColorNewRgb(L *lua.LState) int { + r := L.CheckInt(1) + g := L.CheckInt(2) + b := L.CheckInt(3) + + color := tcell.NewRGBColor(int32(r), int32(g), int32(b)) + + return lAddTcellColorToState(L, &color) +} + +// luaTcellColorNewHex creates color userdata with hexadecimal integer value. +func luaTcellColorNewHex(L *lua.LState) int { + hex := L.CheckInt64(1) + color := tcell.NewHexColor(int32(hex)) + return lAddTcellColorToState(L, &color) +} + +// luaTcellColorNewName creates a color with color name or hex code starting with +// `#`. +func luaTcellColorNewName(L *lua.LState) int { + name := L.CheckString(1) + color := tcell.GetColor(name) + return lAddTcellColorToState(L, &color) +} + +// luaTcellColorNewPalette creates new color with paletter index value. +func luaTcellColorNewPalette(L *lua.LState) int { + index := L.CheckInt(1) + color := tcell.PaletteColor(index) + return lAddTcellColorToState(L, &color) +} + +// ---------------------------------------------------------------------------- +// type tcell.Screen + +const luaTcellScreenTypeName = "tcell.Screen" + +func lRegisterTcellScreenType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaTcellScreenTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{})) + + return mt +} + +func lCheckTcellScreen(L *lua.LState, index int) tcell.Screen { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(tcell.Screen); ok { + return v + } + + L.ArgError(index, "value of type `TcellScreen` expected") + + return nil +} + +func lWrapTcellScreen(L *lua.LState, data tcell.Screen) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaTcellScreenTypeName)) + + return ud +} + +func lAddTcellScreenToState(L *lua.LState, data tcell.Screen) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapTcellScreen(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- diff --git a/lua_binding_time.go b/lua_binding_time.go new file mode 100644 index 00000000..74aabeaa --- /dev/null +++ b/lua_binding_time.go @@ -0,0 +1,733 @@ +package main + +import ( + "time" + + lua "github.com/yuin/gopher-lua" +) + +// ---------------------------------------------------------------------------- +// Type time.Time + +const luaTimeTypeName = "time.Time" + +func lRegisterTimeType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaTimeTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "now": luaTimeNow, + "new_unix": luaTimeNewUnix, + "new_unix_mili": luaTimeNewUnixMili, + "new_unix_micro": luaTimeNewUnixMicro, + + "since_time": luaTimeSince, + "until_time": luaTimeUntil, + + "__eq": luaTimeMetaEq, + "__lt": luaTimeMetaLt, + "__le": luaTimeMetaLe, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "is_zero": luaTimeIsZero, + "compare": luaTimeCompare, + + "date": luaTimeDate, + "year": luaTimeYear, + "month": luaTimeMonth, + "day": luaTimeDay, + "weekday": luaTimeWeekday, + "iso_week": luaTimeISOWeek, + + "clock": luaTimeClock, + "hour": luaTimeHour, + "minute": luaTimeMinute, + "second": luaTimeSecond, + "nanosecond": luaTimeNanosecond, + + "year_day": luaTimeYearDay, + + "add": luaTimeAdd, + "sub": luaTimeSub, + "add_date": luaTimeAddDate, + + "utc": luaTimeUTC, + "local_time": luaLocal, + "time_zone": luaTimeZone, + "time_zone_bounds": luaTimeZoneBounds, + + "to_unix": luaTimeUnix, + "to_unix_mili": luaTimeUnixMili, + "to_unix_nano": luaTimeUnixNano, + + "format": luaTimeFormat, + })) + + return mt +} + +func lCheckTime(L *lua.LState, index int) *time.Time { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*time.Time); ok { + return v + } + + L.ArgError(index, "value of type `Time` expected") + + return nil +} + +func lWrapTime(L *lua.LState, data *time.Time) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaTimeTypeName)) + + return ud +} + +func lAddTimeToState(L *lua.LState, data *time.Time) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapTime(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaTimeNow(L *lua.LState) int { + t := time.Now() + return lAddTimeToState(L, &t) +} + +func luaTimeNewUnix(L *lua.LState) int { + sec := L.CheckInt64(1) + nsec := L.CheckInt64(2) + t := time.Unix(sec, nsec) + return lAddTimeToState(L, &t) +} + +func luaTimeNewUnixMili(L *lua.LState) int { + millis := L.CheckInt64(1) + t := time.UnixMilli(millis) + return lAddTimeToState(L, &t) +} + +func luaTimeNewUnixMicro(L *lua.LState) int { + micro := L.CheckInt64(1) + t := time.UnixMicro(micro) + return lAddTimeToState(L, &t) +} + +func luaTimeSince(L *lua.LState) int { + t := lCheckTime(L, 1) + return lAddDurationToState(L, time.Since(*t)) +} + +func luaTimeUntil(L *lua.LState) int { + t := lCheckTime(L, 1) + return lAddDurationToState(L, time.Until(*t)) +} + +func luaTimeMetaEq(L *lua.LState) int { + self := lCheckTime(L, 1) + other := lCheckTime(L, 2) + L.Push(lua.LBool(self.Equal(*other))) + return 1 +} + +func luaTimeMetaLt(L *lua.LState) int { + self := lCheckTime(L, 1) + other := lCheckTime(L, 2) + L.Push(lua.LBool(self.Before(*other))) + return 1 +} + +func luaTimeMetaLe(L *lua.LState) int { + self := lCheckTime(L, 1) + other := lCheckTime(L, 2) + after := self.After(*other) + L.Push(lua.LBool(!after)) + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaTimeIsZero(L *lua.LState) int { + t := lCheckTime(L, 1) + L.Push(lua.LBool(t.IsZero())) + return 1 +} + +func luaTimeCompare(L *lua.LState) int { + self := lCheckTime(L, 1) + other := lCheckTime(L, 2) + L.Push(lua.LNumber(self.Compare(*other))) + return 1 +} + +func luaTimeDate(L *lua.LState) int { + t := lCheckTime(L, 1) + year, month, day := t.Date() + L.Push(lua.LNumber(year)) + lAddMonthToState(L, month) + L.Push(lua.LNumber(day)) + return 3 +} + +func luaTimeYear(L *lua.LState) int { + t := lCheckTime(L, 1) + year := t.Year() + L.Push(lua.LNumber(year)) + return 1 +} + +func luaTimeMonth(L *lua.LState) int { + t := lCheckTime(L, 1) + month := t.Month() + return lAddMonthToState(L, month) +} + +func luaTimeDay(L *lua.LState) int { + t := lCheckTime(L, 1) + day := t.Day() + L.Push(lua.LNumber(day)) + return 1 +} + +func luaTimeWeekday(L *lua.LState) int { + t := lCheckTime(L, 1) + weekday := t.Weekday() + return lAddWeekdayToState(L, weekday) +} + +func luaTimeISOWeek(L *lua.LState) int { + t := lCheckTime(L, 1) + year, week := t.ISOWeek() + L.Push(lua.LNumber(year)) + L.Push(lua.LNumber(week)) + return 2 +} + +func luaTimeClock(L *lua.LState) int { + t := lCheckTime(L, 1) + hour, min, sec := t.Clock() + L.Push(lua.LNumber(hour)) + L.Push(lua.LNumber(min)) + L.Push(lua.LNumber(sec)) + return 3 +} + +func luaTimeHour(L *lua.LState) int { + t := lCheckTime(L, 1) + hour := t.Hour() + L.Push(lua.LNumber(hour)) + return 1 +} + +func luaTimeMinute(L *lua.LState) int { + t := lCheckTime(L, 1) + minute := t.Minute() + L.Push(lua.LNumber(minute)) + return 1 +} + +func luaTimeSecond(L *lua.LState) int { + t := lCheckTime(L, 1) + second := t.Second() + L.Push(lua.LNumber(second)) + return 1 +} + +func luaTimeNanosecond(L *lua.LState) int { + t := lCheckTime(L, 1) + nanosecond := t.Nanosecond() + L.Push(lua.LNumber(nanosecond)) + return 1 +} + +func luaTimeYearDay(L *lua.LState) int { + t := lCheckTime(L, 1) + yearDay := t.YearDay() + L.Push(lua.LNumber(yearDay)) + return 1 +} + +func luaTimeAdd(L *lua.LState) int { + t := lCheckTime(L, 1) + dur := lCheckDuration(L, 2) + result := t.Add(dur) + return lAddTimeToState(L, &result) +} + +func luaTimeSub(L *lua.LState) int { + t := lCheckTime(L, 1) + other := lCheckTime(L, 2) + result := t.Sub(*other) + return lAddDurationToState(L, result) +} + +func luaTimeAddDate(L *lua.LState) int { + t := lCheckTime(L, 1) + years := L.CheckInt(2) + months := L.CheckInt(3) + days := L.CheckInt(4) + + result := t.AddDate(years, months, days) + return lAddTimeToState(L, &result) +} + +func luaTimeUTC(L *lua.LState) int { + t := lCheckTime(L, 1) + result := t.UTC() + return lAddTimeToState(L, &result) +} + +func luaLocal(L *lua.LState) int { + t := lCheckTime(L, 1) + result := t.Local() + return lAddTimeToState(L, &result) +} + +func luaTimeZone(L *lua.LState) int { + t := lCheckTime(L, 1) + name, offset := t.Zone() + L.Push(lua.LString(name)) + L.Push(lua.LNumber(offset)) + return 2 +} + +func luaTimeZoneBounds(L *lua.LState) int { + t := lCheckTime(L, 1) + minOffset, maxOffset := t.ZoneBounds() + lAddTimeToState(L, &minOffset) + lAddTimeToState(L, &maxOffset) + return 2 +} + +func luaTimeUnix(L *lua.LState) int { + t := lCheckTime(L, 1) + sec := t.Unix() + L.Push(lua.LNumber(sec)) + return 1 +} + +func luaTimeUnixMili(L *lua.LState) int { + t := lCheckTime(L, 1) + millis := t.UnixMilli() + L.Push(lua.LNumber(millis)) + return 1 +} + +func luaTimeUnixNano(L *lua.LState) int { + t := lCheckTime(L, 1) + nanos := t.UnixNano() + L.Push(lua.LNumber(nanos)) + return 1 +} + +func luaTimeFormat(L *lua.LState) int { + t := lCheckTime(L, 1) + fmtStr := L.CheckString(2) + L.Push(lua.LString(t.Format(fmtStr))) + return 1 +} + +// ---------------------------------------------------------------------------- +// type time.Month + +const luaMonthTypeName = "time.Month" + +func lRegisterMonthType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaMonthTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "__tostring": luaMonthMetaTostring, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{})) + + return mt +} + +func lCheckMonth(L *lua.LState, index int) time.Month { + value := L.Get(index) + switch value.Type() { + case lua.LTNumber: + mo := time.Month(value.(lua.LNumber)) + return mo + case lua.LTUserData: + if v, ok := value.(*lua.LUserData).Value.(time.Month); ok { + return v + } + } + + L.ArgError(index, "value of type `Month` expected") + + return 0 +} + +func lWrapMonth(L *lua.LState, data time.Month) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaMonthTypeName)) + + return ud +} + +func lAddMonthToState(L *lua.LState, data time.Month) int { + ud := lWrapMonth(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaMonthMetaTostring(L *lua.LState) int { + month := lCheckMonth(L, 1) + L.Push(lua.LString(month.String())) + return 1 +} + +// ---------------------------------------------------------------------------- + +const luaWeekdayTypeName = "time.Weekday" + +func lRegisterWeekdayType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaWeekdayTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "__tostring": luaWeekdayMetaTostring, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{})) + + return mt +} + +func lCheckWeekday(L *lua.LState, index int) time.Weekday { + value := L.Get(index) + switch value.Type() { + case lua.LTNumber: + wd := time.Weekday(value.(lua.LNumber)) + return wd + case lua.LTUserData: + if v, ok := value.(*lua.LUserData).Value.(time.Weekday); ok { + return v + } + } + + L.ArgError(index, "value of type `Weekday` expected") + + return 0 +} + +func lWrapWeekday(L *lua.LState, data time.Weekday) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaWeekdayTypeName)) + + return ud +} + +func lAddWeekdayToState(L *lua.LState, data time.Weekday) int { + ud := lWrapWeekday(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaWeekdayMetaTostring(L *lua.LState) int { + weekday := lCheckWeekday(L, 1) + L.Push(lua.LString(weekday.String())) + return 1 +} + +// ---------------------------------------------------------------------------- +// type time.Duration + +const luaDurationTypeName = "time.Duration" + +func lRegisterDurationType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaDurationTypeName) + + addDurationConstantToMt(L, mt) + + L.SetFuncs(mt, map[string]lua.LGFunction{ + "new": luaDurationNew, + + "__add": luaDurationMetaAdd, + "__sub": luaDurationMetaSub, + "__mul": luaDurationMetaMul, + "__div": luaDurationMetaDiv, + + "__eq": luaDurationMetaEq, + "__lt": luaDurationMetaLt, + "__le": luaDurationMetaLe, + + "__tostring": luaDurationMetaTostring, + }) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "add": luaDurationMetaAdd, + "sub": luaDurationMetaSub, + "mul": luaDurationMetaMul, + "div": luaDurationMetaDiv, + "eq": luaDurationMetaEq, + "lt": luaDurationMetaLt, + "le": luaDurationMetaLe, + + "nanoseconds": luaDurationNanoseconds, + "microseconds": luadurationMicroseconds, + "milliseconds": luaDurationMilliseconds, + "seconds": luaDurationSeconds, + "minutes": luaDurationMinutes, + "hours": luaDurationHours, + "truncate": luaDurationTruncate, + "round": luaDurationRound, + "abs": luaDurationAbs, + + "to_number": luaDurationToNumber, + })) + + return mt +} + +func addDurationConstantToMt(L *lua.LState, tbl *lua.LTable) { + tbl.RawSetString("Nanosecond", lWrapDuration(L, time.Nanosecond)) + tbl.RawSetString("Microsecond", lWrapDuration(L, time.Microsecond)) + tbl.RawSetString("Millisecond", lWrapDuration(L, time.Millisecond)) + tbl.RawSetString("Second", lWrapDuration(L, time.Second)) + tbl.RawSetString("Minute", lWrapDuration(L, time.Minute)) + tbl.RawSetString("Hour", lWrapDuration(L, time.Hour)) +} + +func lCheckDuration(L *lua.LState, index int) time.Duration { + value := L.Get(index) + switch value.Type() { + case lua.LTNumber: + dur := time.Duration(value.(lua.LNumber)) + return dur + case lua.LTUserData: + if v, ok := value.(*lua.LUserData).Value.(time.Duration); ok { + return v + } + } + + L.ArgError(index, "value of type `Duration` expected") + + return 0 +} + +func lWrapDuration(L *lua.LState, data time.Duration) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaDurationTypeName)) + + return ud +} + +func lAddDurationToState(L *lua.LState, data time.Duration) int { + ud := lWrapDuration(L, data) + L.Push(ud) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaDurationNew(L *lua.LState) int { + value := L.CheckNumber(1) + dur := time.Duration(value) + return lAddDurationToState(L, dur) +} + +func luaDurationMetaAdd(L *lua.LState) int { + self := lCheckDuration(L, 1) + other := lCheckDuration(L, 2) + result := self + other + return lAddDurationToState(L, result) +} + +func luaDurationMetaSub(L *lua.LState) int { + self := lCheckDuration(L, 1) + other := lCheckDuration(L, 2) + result := self - other + return lAddDurationToState(L, result) +} + +func luaDurationMetaMul(L *lua.LState) int { + self := lCheckDuration(L, 1) + other := lCheckDuration(L, 2) + result := self * other + return lAddDurationToState(L, result) +} + +func luaDurationMetaDiv(L *lua.LState) int { + self := lCheckDuration(L, 1) + other := lCheckDuration(L, 2) + L.Push(lua.LNumber(float64(self) / float64(other))) + return 1 +} + +func luaDurationMetaEq(L *lua.LState) int { + self := lCheckDuration(L, 1) + other := lCheckDuration(L, 2) + L.Push(lua.LBool(self == other)) + return 1 +} + +func luaDurationMetaLt(L *lua.LState) int { + self := lCheckDuration(L, 1) + other := lCheckDuration(L, 2) + L.Push(lua.LBool(self < other)) + return 1 +} + +func luaDurationMetaLe(L *lua.LState) int { + self := lCheckDuration(L, 1) + other := lCheckDuration(L, 2) + L.Push(lua.LBool(self <= other)) + return 1 +} + +func luaDurationMetaTostring(L *lua.LState) int { + dur := lCheckDuration(L, 1) + L.Push(lua.LString(dur.String())) + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaDurationNanoseconds(L *lua.LState) int { + dur := lCheckDuration(L, 1) + L.Push(lua.LNumber(dur.Nanoseconds())) + return 1 +} + +func luadurationMicroseconds(L *lua.LState) int { + dur := lCheckDuration(L, 1) + L.Push(lua.LNumber(dur.Microseconds())) + return 1 +} + +func luaDurationMilliseconds(L *lua.LState) int { + dur := lCheckDuration(L, 1) + L.Push(lua.LNumber(dur.Milliseconds())) + return 1 +} + +func luaDurationSeconds(L *lua.LState) int { + dur := lCheckDuration(L, 1) + L.Push(lua.LNumber(dur.Seconds())) + return 1 +} + +func luaDurationMinutes(L *lua.LState) int { + dur := lCheckDuration(L, 1) + L.Push(lua.LNumber(dur.Minutes())) + return 1 +} + +func luaDurationHours(L *lua.LState) int { + dur := lCheckDuration(L, 1) + L.Push(lua.LNumber(dur.Hours())) + return 1 +} + +func luaDurationTruncate(L *lua.LState) int { + dur := lCheckDuration(L, 1) + m := lCheckDuration(L, 2) + result := dur.Truncate(m) + return lAddDurationToState(L, result) +} + +func luaDurationRound(L *lua.LState) int { + dur := lCheckDuration(L, 1) + m := lCheckDuration(L, 2) + result := dur.Round(m) + return lAddDurationToState(L, result) +} + +func luaDurationAbs(L *lua.LState) int { + dur := lCheckDuration(L, 1) + return lAddDurationToState(L, dur.Abs()) +} + +// luaDurationToNumber converts duration userdata to number value. +func luaDurationToNumber(L *lua.LState) int { + duration := lCheckDuration(L, 1) + L.Push(lua.LNumber(duration)) + return 1 +} + +// ---------------------------------------------------------------------------- +// type time.Timer + +const luaTimerTypeName = "time.Timer" + +func lRegisterTimerType(L *lua.LState) *lua.LTable { + mt := L.NewTypeMetatable(luaTimerTypeName) + + L.SetFuncs(mt, map[string]lua.LGFunction{}) + L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "reset": luaTimerReset, + "stop": luaTimerStop, + })) + + return mt +} + +func lCheckTimer(L *lua.LState, index int) *time.Timer { + ud := L.CheckUserData(index) + if v, ok := ud.Value.(*time.Timer); ok { + return v + } + + L.ArgError(index, "value of type `Timer` expected") + + return nil +} + +func lWrapTimer(L *lua.LState, data *time.Timer) *lua.LUserData { + ud := L.NewUserData() + ud.Value = data + + L.SetMetatable(ud, L.GetTypeMetatable(luaTimerTypeName)) + + return ud +} + +/* func lAddTimerToState(L *lua.LState, data *time.Timer) int { + if data == nil { + L.Push(lua.LNil) + return 1 + } + + ud := lWrapTimer(L, data) + L.Push(ud) + + return 1 +} */ + +// ---------------------------------------------------------------------------- + +func luaTimerReset(L *lua.LState) int { + timer := lCheckTimer(L, 1) + duration := lCheckDuration(L, 2) + L.Push(lua.LBool(timer.Reset(duration))) + return 1 +} + +func luaTimerStop(L *lua.LState) int { + timer := lCheckTimer(L, 1) + L.Push(lua.LBool(timer.Stop())) + return 1 +} diff --git a/lua_module_fs.go b/lua_module_fs.go new file mode 100644 index 00000000..96d63c83 --- /dev/null +++ b/lua_module_fs.go @@ -0,0 +1,247 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + + lua "github.com/yuin/gopher-lua" +) + +func lfFsModuleLoader(L *lua.LState) int { + mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "mkdir": luaFsMkdir, + "mkdir_all": luaFsModuleMkdirAll, + "link": luaFsModuleLink, + "symlink": luaFsModuleSymlink, + "copy": luaFsModuleCopyFile, + + "join": luaFsModuleJoin, + "split": luaFsModuleSplit, + "split_ext": luaFsModuleSplitExt, + "dirname": luaFsModuleDirname, + "basename": luaFsModuleBasename, + "ext": luaFsModuleExt, + + "stat": luaFsModuleStat, + "readdir": luaFsModuleReadDir, + }) + + L.Push(mod) + + return 1 +} + +func luaFsMkdir(L *lua.LState) int { + path := L.CheckString(1) + + if err := os.Mkdir(path, 0o777); err == nil { + L.Push(lua.LNil) + } else { + L.Push(lua.LString(err.Error())) + } + + return 1 +} + +func luaFsModuleMkdirAll(L *lua.LState) int { + path := L.CheckString(1) + + if err := os.MkdirAll(path, 0o777); err == nil { + L.Push(lua.LNil) + } else { + L.Push(lua.LString(err.Error())) + } + + return 1 +} + +func luaFsModuleLink(L *lua.LState) int { + oldname := L.CheckString(1) + newname := L.CheckString(2) + force := L.OptBool(3, false) + + if stat, err := os.Lstat(newname); err == nil { + if stat.Mode()&os.ModeSymlink == 0 { + L.Push(lua.LString(fmt.Sprintf("%s already exists and is not a symlink", newname))) + return 1 + } + + if force { + err := os.Remove(newname) + if err != nil { + L.Push(lua.LString(fmt.Sprintf("failed to remove existing link: %s", err))) + return 1 + } + } else { + L.Push(lua.LString(fmt.Sprintf("link %s already exists", newname))) + return 1 + } + } + + if err := os.Link(oldname, newname); err == nil { + L.Push(lua.LNil) + } else { + L.Push(lua.LString(err.Error())) + } + + return 1 +} + +func luaFsModuleSymlink(L *lua.LState) int { + oldname := L.CheckString(1) + newname := L.CheckString(2) + force := L.OptBool(3, false) + + if stat, err := os.Lstat(newname); err == nil { + if stat.Mode()&os.ModeSymlink == 0 { + L.Push(lua.LString(fmt.Sprintf("%s already exists and is not a symlink", newname))) + return 1 + } + + if force { + err := os.Remove(newname) + if err != nil { + L.Push(lua.LString(fmt.Sprintf("failed to remove existing link: %s", err))) + return 1 + } + } else { + L.Push(lua.LString(fmt.Sprintf("link %s already exists", newname))) + return 1 + } + } + + if err := os.Symlink(oldname, newname); err == nil { + L.Push(lua.LNil) + } else { + L.Push(lua.LString(err.Error())) + } + + return 1 +} + +func luaFsModuleCopyFile(L *lua.LState) int { + src := L.CheckString(1) + dst := L.CheckString(2) + + srcFile, err := os.Open(src) + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + defer srcFile.Close() + + dstFile, err := os.Create(dst) + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + defer dstFile.Close() + + reader := bufio.NewReader(srcFile) + writer := bufio.NewWriter(dstFile) + + _, err = io.Copy(writer, reader) + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + err = writer.Flush() + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + + return 0 +} + +func luaFsModuleJoin(L *lua.LState) int { + cnt := L.GetTop() + parts := []string{} + + for i := 1; i <= cnt; i++ { + parts = append(parts, L.CheckString(i)) + } + + result := filepath.Join(parts...) + L.Push(lua.LString(result)) + + return 1 +} + +func luaFsModuleSplit(L *lua.LState) int { + path := L.CheckString(1) + dirname, basename := filepath.Split(path) + L.Push(lua.LString(dirname)) + L.Push(lua.LString(basename)) + return 2 +} + +func luaFsModuleSplitExt(L *lua.LState) int { + path := L.CheckString(1) + ext := filepath.Ext(path) + stem := path[:len(path)-len(ext)] + L.Push(lua.LString(stem)) + L.Push(lua.LString(ext)) + return 2 +} + +func luaFsModuleDirname(L *lua.LState) int { + path := L.CheckString(1) + result := filepath.Dir(path) + L.Push(lua.LString(result)) + return 1 +} + +func luaFsModuleBasename(L *lua.LState) int { + path := L.CheckString(1) + result := filepath.Base(path) + L.Push(lua.LString(result)) + return 1 +} + +func luaFsModuleExt(L *lua.LState) int { + path := L.CheckString(1) + result := filepath.Ext(path) + L.Push(lua.LString(result)) + return 1 +} + +func luaFsModuleStat(L *lua.LState) int { + path := L.CheckString(1) + + stat, err := os.Stat(path) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + lAddFileInfoToState(L, stat) + + return 1 +} + +func luaFsModuleReadDir(L *lua.LState) int { + path := L.CheckString(1) + + tbl := L.NewTable() + + entries, err := os.ReadDir(path) + if err != nil { + L.Push(tbl) + L.Push(lua.LString(err.Error())) + return 2 + } + + for _, entry := range entries { + tbl.Append(lWrapDirEntry(L, entry)) + } + + L.Push(tbl) + + return 1 +} diff --git a/lua_module_main.go b/lua_module_main.go new file mode 100644 index 00000000..6f3cef4e --- /dev/null +++ b/lua_module_main.go @@ -0,0 +1,768 @@ +package main + +import ( + "fmt" + "log" + "path/filepath" + "reflect" + "slices" + "strconv" + "strings" + + lua "github.com/yuin/gopher-lua" +) + +func lfMainModuleLoader(L *lua.LState) int { + mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "print": luaMainModulePrint, + "list_ext": luaMainModuleListExtend, + "tbl_extend": luaMainModuleTblExtend, + + "cmd": luaMainModuleRunColonCommand, + "shell": luaMainModuleRunShellCommand, + "call": luaMainModuleCallCommand, + "call_n": luaMainModuleCallCommandN, + + "set_opt": luaMainModuleSetOptionValue, + "set_local_opt": luaMainModuleSetLocalOptionValue, + "get_opt": luaMainModuleGetOptionValue, + "get_local_opt": luaMainModuleGetLocalOptionValue, + + "call_msg_expr": luaMainModuleCallMsgExpr, + + "glob_match": luaMainModuleGlobMatch, + + "match_word": luaMainModuleMatchWord, + "match_list": luaMainModuleMatchList, + "match_cmd": luaMainModuleMatchCmd, + "match_cmd_file": luaMainModuleMatchCmdFile, + "match_shell_file": luaMainModuleMatchShellFile, + "match_exec": luaMainModuleMatchExec, + "match_search": luaMainModuleMatchSearch, + "match_opt_name": luaMainModuleMatchOptName, + "match_local_opt_name": luaMainModuleMatchLocalOptName, + + "str_fill": luaMainModuleStrFill, + "str_fill_right": luaMainModuleStrFillRight, + "to_perm_string": luaMainModuleToPermString, + "make_link_count_str": luaMainModuleMakeLinkCountStr, + "make_user_name_str": luaMainModuleMakeUserNameStr, + "make_group_name_str": luaMainModuleMakeGroupNameStr, + "sanitize_name": luaMainModuleSanitizeName, + "file_size_humanize": luaMainModuleFileSizeHumanize, + "disk_free_space": luaMainModuleDiskFreeSpace, + }) + + setupModuleConstants(L, mod) + + L.Push(mod) + + return 1 +} + +func setupModuleConstants(L *lua.LState, mod *lua.LTable) { + // constant + mod.RawSetString("PREVIEW_LOADING_DELAY", lua.LNumber(previewLoadingDelay)) + + // clipboard mode + clipboardMode := L.NewTable() + clipboardMode.RawSetString("Copy", lua.LNumber(clipboardCopy)) + clipboardMode.RawSetString("Cut", lua.LNumber(clipboardCut)) + mod.RawSetString("ClipboardMode", clipboardMode) + + // dir role + dirRole := L.NewTable() + dirRole.RawSetString("Active", lua.LNumber(Active)) + dirRole.RawSetString("Parent", lua.LNumber(Parent)) + dirRole.RawSetString("Preview", lua.LNumber(Preview)) + mod.RawSetString("DirRole", dirRole) + + // event type + eventType := L.NewTable() + eventType.RawSetString("PreCd", lua.LString("pre-cd")) + eventType.RawSetString("OnCd", lua.LString("on-cd")) + eventType.RawSetString("OnLoad", lua.LString("on-load")) + eventType.RawSetString("OnFocus-gained", lua.LString("on-focus-gained")) + eventType.RawSetString("OnFocus-lost", lua.LString("on-focus-lost")) + eventType.RawSetString("OnInit", lua.LString("on-init")) + eventType.RawSetString("OnRedraw", lua.LString("on-redraw")) + eventType.RawSetString("OnSelect", lua.LString("on-select")) + eventType.RawSetString("OnQuit", lua.LString("on-quit")) + mod.RawSetString("EventType", eventType) + + // shell command type + shellCmdType := L.NewTable() + shellCmdType.RawSetString("Normal", lua.LString("$")) + shellCmdType.RawSetString("Pipe", lua.LString("%")) + shellCmdType.RawSetString("Wait", lua.LString("!")) + shellCmdType.RawSetString("Async", lua.LString("&")) + mod.RawSetString("ShellCmdType", shellCmdType) + + // sort method name + sortMethodNames := L.NewTable() + sortMethodNames.RawSetString("Natural", lua.LString(naturalSort)) + sortMethodNames.RawSetString("Name", lua.LString(nameSort)) + sortMethodNames.RawSetString("Size", lua.LString(sizeSort)) + sortMethodNames.RawSetString("Time", lua.LString(timeSort)) + sortMethodNames.RawSetString("Atime", lua.LString(atimeSort)) + sortMethodNames.RawSetString("Btimed", lua.LString(btimeSort)) + sortMethodNames.RawSetString("Ctime", lua.LString(ctimeSort)) + sortMethodNames.RawSetString("Ext", lua.LString(extSort)) + sortMethodNames.RawSetString("Custom", lua.LString(customSort)) + mod.RawSetString("SortMethod", sortMethodNames) + + // key map type + keyMapType := L.NewTable() + keyMapType.RawSetString("Normal", lua.LString(luaKeyMapTypeNormal)) + keyMapType.RawSetString("Visual", lua.LString(luaKeyMapTypeVisual)) + keyMapType.RawSetString("Command", lua.LString(luaKeyMapTypeCommand)) + mod.RawSetString("KeyMapType", keyMapType) + + // ui formatter type + uiFormatterType := L.NewTable() + uiFormatterType.RawSetString("cursoractive", lua.LString(luaUIFormatterCursorActive)) + uiFormatterType.RawSetString("cursorparent", lua.LString(luaUIFormatterCursorParent)) + uiFormatterType.RawSetString("cursorpreview", lua.LString(luaUIFormatterCursorPreview)) + uiFormatterType.RawSetString("error", lua.LString(luaUIFormatterError)) + uiFormatterType.RawSetString("numbercursor", lua.LString(luaUIFormatterNumberCursor)) + uiFormatterType.RawSetString("number", lua.LString(luaUIFormatterNumber)) + uiFormatterType.RawSetString("tag", lua.LString(luaUIFormatterTag)) + mod.RawSetString("UIFormatterType", uiFormatterType) + + // ui printer type + uiPrinterType := L.NewTable() + uiPrinterType.RawSetString("dir_entry", lua.LString(luaUIPrinterDirEntry)) + uiPrinterType.RawSetString("directory", lua.LString(luaUIPrinterDirectory)) + uiPrinterType.RawSetString("ruler", lua.LString(luaUIPrinterRuler)) + uiPrinterType.RawSetString("prompt", lua.LString(luaUIPrinterPrompt)) + mod.RawSetString("UIPrinterType", uiPrinterType) + + // ui style type + uiStyleType := L.NewTable() + uiStyleType.RawSetString("border", lua.LString(luaUIStyleBorder)) + uiStyleType.RawSetString("copy", lua.LString(luaUIStyleCopy)) + uiStyleType.RawSetString("cut", lua.LString(luaUIStyleCut)) + uiStyleType.RawSetString("menu", lua.LString(luaUIStyleMenu)) + uiStyleType.RawSetString("menuheader", lua.LString(luaUIStyleMenuheader)) + uiStyleType.RawSetString("menuselect", lua.LString(luaUIStyleMenuselect)) + uiStyleType.RawSetString("select", lua.LString(luaUIStyleSelect)) + uiStyleType.RawSetString("visual", lua.LString(luaUIStyleVisual)) + mod.RawSetString("UIStyleType", uiStyleType) +} + +// ---------------------------------------------------------------------------- + +func prettyPrintLuaValue(builder *strings.Builder, val lua.LValue, visited map[lua.LValue]int, tableCnt, indentLevel int) int { + switch val.Type() { + case lua.LTNil, lua.LTBool, lua.LTNumber, lua.LTFunction, lua.LTUserData, lua.LTThread, lua.LTChannel: + builder.WriteString(val.String()) + case lua.LTString: + fmt.Fprintf(builder, "%q", val.String()) + case lua.LTTable: + mark := visited[val] + if mark > 0 { + builder.WriteString("table<") + builder.WriteString(strconv.Itoa(mark)) + builder.WriteString(">") + return tableCnt + } + + tableCnt++ + visited[val] = tableCnt + + builder.WriteString("{") + + isEmpty := true + val.(*lua.LTable).ForEach(func(key, value lua.LValue) { + isEmpty = false + + builder.WriteString("\n") + for range indentLevel + 1 { + builder.WriteString(" ") + } + tableCnt = prettyPrintLuaValue(builder, key, visited, tableCnt, indentLevel+1) + builder.WriteString(" = ") + tableCnt = prettyPrintLuaValue(builder, value, visited, tableCnt, indentLevel+1) + builder.WriteString(",") + }) + + if !isEmpty { + builder.WriteString("\n") + for range indentLevel { + builder.WriteString(" ") + } + } + builder.WriteString("}") + } + + return tableCnt +} + +// luaMainModulePrint prints a Lua value, this can be used for debugging. +func luaMainModulePrint(L *lua.LState) int { + value := L.CheckAny(1) + var builder strings.Builder + prettyPrintLuaValue(&builder, value, make(map[lua.LValue]int), 0, 0) + log.Println(builder.String()) + return 0 +} + +// luaMainModuleListExtend takes 2 table, append all elements in `src` table into +// `dst` table. +func luaMainModuleListExtend(L *lua.LState) int { + dst := L.CheckTable(1) + src := L.CheckTable(2) + stValue := L.Get(3) + edValue := L.Get(4) + + nElem := src.Len() + st := 1 + ed := nElem + + if stValue.Type() == lua.LTNumber { + st = int(stValue.(lua.LNumber)) + if st < 1 { + st = 0 + } + } + if edValue.Type() == lua.LTNumber { + ed = int(edValue.(lua.LNumber)) + if ed > nElem { + ed = nElem + } + } + + for i := st; i <= ed; i++ { + dst.Append(src.RawGetInt(i)) + } + + L.Push(dst) + + return 1 +} + +// luaMainModuleTableExtend merges two or more tables. +func luaMainModuleTblExtend(L *lua.LState) int { + behaviorValue := L.CheckAny(1) + + var behavior string + var checker *lua.LFunction + + switch behaviorValue.Type() { + case lua.LTString: + behavior = string(behaviorValue.(lua.LString)) + switch behavior { + case "error", "keep", "force": + // ok + default: + L.ArgError(1, "unsupport behavior value") + } + case lua.LTFunction: + checker = behaviorValue.(*lua.LFunction) + default: + L.ArgError(1, "expected string or function") + return 0 + } + + dst := L.NewTable() + + offset := 2 + nArgs := L.GetTop() + for i := offset; i <= nArgs; i++ { + tbl := L.CheckTable(i) + tbl.ForEach(func(key, value lua.LValue) { + oldValue := dst.RawGet(key) + + if checker != nil { + _ = L.CallByParam( + lua.P{ + Fn: checker, + NRet: 1, + }, + key, + oldValue, + value, + ) + + newValue := L.Get(-1) + L.Pop(1) + + dst.RawSet(key, newValue) + } else if oldValue == lua.LNil { + dst.RawSet(key, value) + } else { + switch behavior { + case "error": + L.RaiseError("key duplicated: %s", key) + case "keep": + // pass + case "force": + dst.RawSet(key, value) + } + } + }) + } + + L.Push(dst) + + return 1 +} + +// ---------------------------------------------------------------------------- + +// luaMainModuleRunColonCommand runs a lf command string just like calling command with `:`. +func luaMainModuleRunColonCommand(L *lua.LState) int { + cmd := L.CheckString(1) + + app, err := getAppObjectFromLuaGlobals(L) + if err != nil { + L.RaiseError("failed to get app object: %s", err) + return 0 + } + + p := newParser(strings.NewReader(cmd)) + for p.parse() { + app.ui.exprChan <- p.expr + } + if p.err != nil { + app.ui.echoerrf("%s", p.err) + } + + return 0 +} + +// luaMainModuleRunShellCommand takes execution type prefix, command name and variable length +// argument list, and asks lf to execute given shell command. +func luaMainModuleRunShellCommand(L *lua.LState) int { + prefix := L.CheckString(1) + cmd := L.CheckString(2) + + app, err := getAppObjectFromLuaGlobals(L) + if err != nil { + L.RaiseError("failed to get app object: %s", err) + return 0 + } + + st := 3 + nArgs := L.GetTop() + args := make([]string, nArgs-st+1) + for i := st; i <= nArgs; i++ { + arg := L.Get(i) + args[i-st] = arg.String() + } + + switch prefix { + case "$": + log.Printf("shell: %s -- %q", cmd, args) + app.runShell(cmd, args, prefix) + case "%": + log.Printf("shell-pipe: %s -- %q", cmd, args) + app.runShell(cmd, args, prefix) + case "!": + log.Printf("shell-wait: %s -- %q", cmd, args) + app.runShell(cmd, args, prefix) + case "&": + log.Printf("shell-async: %s -- %q", cmd, args) + app.runShell(cmd, args, prefix) + default: + log.Printf("unknown execution prefix: %q", prefix) + } + + return 0 +} + +// luaMainModuleCallCommand runs lf command. +func luaMainModuleCallCommand(L *lua.LState) int { + name := L.CheckString(1) + + app, err := getAppObjectFromLuaGlobals(L) + if err != nil { + L.RaiseError("failed to get app object: %s", err) + return 0 + } + + st := 2 + nArgs := L.GetTop() + args := make([]string, nArgs-st+1) + for i := st; i <= nArgs; i++ { + arg := L.Get(i) + args[i-st] = arg.String() + } + + app.ui.exprChan <- &callExpr{name, args, 1} + + return 0 +} + +// luaMainModuleCallCommandN runs lf command with repetition argument `n`. +func luaMainModuleCallCommandN(L *lua.LState) int { + count := L.CheckInt(1) + name := L.CheckString(2) + + app, err := getAppObjectFromLuaGlobals(L) + if err != nil { + L.RaiseError("failed to get app object: %s", err) + return 0 + } + + st := 3 + nArgs := L.GetTop() + args := make([]string, nArgs-st+1) + for i := st; i <= nArgs; i++ { + arg := L.Get(i) + args[i-st] = arg.String() + } + + app.ui.exprChan <- &callExpr{name, args, count} + + return 0 +} + +func luaMainModuleSetOptionValue(L *lua.LState) int { + opt := L.CheckString(1) + val := L.CheckString(2) + + app, err := getAppObjectFromLuaGlobals(L) + if err != nil { + L.RaiseError("failed to get app object: %s", err) + return 0 + } + + app.ui.exprChan <- &setExpr{opt, val} + + return 0 +} + +func luaMainModuleSetLocalOptionValue(L *lua.LState) int { + path := L.CheckString(1) + opt := L.CheckString(2) + val := L.CheckString(3) + + app, err := getAppObjectFromLuaGlobals(L) + if err != nil { + L.RaiseError("failed to get app object: %s", err) + return 0 + } + + app.ui.exprChan <- &setLocalExpr{path, opt, val} + + return 0 +} + +func luaMainModuleGetOptionValue(L *lua.LState) int { + opt := L.CheckString(1) + + rValue := reflect.ValueOf(gOpts) + field := rValue.FieldByName(opt) + + if !field.IsValid() { + L.Push(lua.LNil) + L.Push(lua.LString(fmt.Sprintf("option %q does not exist", opt))) + return 2 + } + + luaValue, err := goReflectValueToLuaValue(L, field) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(fmt.Sprintf("error converting option value: %s", err))) + return 2 + } + + L.Push(luaValue) + + return 1 +} + +func luaMainModuleGetLocalOptionValue(L *lua.LState) int { + path := L.CheckString(1) + opt := L.CheckString(2) + + rValue := reflect.ValueOf(gLocalOpts) + field := rValue.FieldByName(opt) + + if !field.IsValid() { + L.Push(lua.LNil) + L.Push(lua.LString(fmt.Sprintf("option %q does not exist", opt))) + return 2 + } + + if field.Kind() != reflect.Map { + L.Push(lua.LNil) + L.Push(lua.LString(fmt.Sprintf("option %q is not a map field", opt))) + return 2 + } + + value := field.MapIndex(reflect.ValueOf(path)) + if !value.IsValid() { + L.Push(lua.LNil) + L.Push(lua.LNil) + return 2 + } + + luaValue, err := goReflectValueToLuaValue(L, value) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(fmt.Sprintf("error converting option value: %s", err))) + return 2 + } + + L.Push(luaValue) + + return 1 +} + +// ---------------------------------------------------------------------------- + +func luaMainModuleCallMsgExpr(L *lua.LState) int { + expr := lCheckLuaMsgExpr(L, 1) + + action, err := getLuaMsgAction(L, expr.sourceName, expr.registry, expr.msg, expr.variant) + if err != nil { + L.RaiseError("%s", err) + return 0 + } + + L.Replace(1, action) + L.Call(L.GetTop()-1, lua.MultRet) + + return L.GetTop() +} + +// ---------------------------------------------------------------------------- + +// luaMainModuleGlobMatch checks if a pattern matches certain string. +func luaMainModuleGlobMatch(L *lua.LState) int { + pattern := L.CheckString(1) + str := L.CheckString(2) + + match, err := filepath.Match(pattern, str) + if err != nil { + L.Push(lua.LFalse) + L.Push(lua.LString(fmt.Sprintf("glob match error: %s", err))) + return 2 + } + + L.Push(lua.LBool(match)) + + return 1 +} + +func luaMainModuleAddMatchResultToLuaState(L *lua.LState, matches []compMatch, longest string) int { + tbl := L.NewTable() + for _, match := range matches { + tbl.Append(lWrapCompMatch(L, &match)) + } + + L.Push(tbl) + L.Push(lua.LString(longest)) + + return 2 +} + +// luaMainModuleMatchWord takes a source string, and a list of candidate string, and +// returns a list of match object and longest common matched string. +func luaMainModuleMatchWord(L *lua.LState) int { + longest := L.CheckString(1) + wordTbl := L.CheckTable(2) + + nWord := wordTbl.Len() + words := make([]string, nWord) + + for i := 1; i <= nWord; i++ { + word := wordTbl.RawGetInt(i) + words[i-1] = word.String() + } + + slices.Sort(words) + matches, longest := matchWord(longest, slices.Compact(words)) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +// luaMainModuleMatchList takes a `:` seperated list string, and a list element +// candidate list, returns matching entry and completion result string. +func luaMainModuleMatchList(L *lua.LState) int { + longest := L.CheckString(1) + wordTbl := L.CheckTable(2) + + nWord := wordTbl.Len() + words := make([]string, nWord) + + for i := 1; i <= nWord; i++ { + word := wordTbl.RawGetInt(i) + words[i-1] = word.String() + } + + slices.Sort(words) + matches, longest := matchList(longest, slices.Compact(words)) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +// luaMainModuleMatchCmd makes command name completion. +func luaMainModuleMatchCmd(L *lua.LState) int { + longest := L.CheckString(1) + + matches, longest := matchCmd(longest) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +func luaMainModuleMatchCmdFile(L *lua.LState) int { + longest := L.CheckString(1) + dirOnly := L.CheckBool(2) + + matches, longest := matchCmdFile(longest, dirOnly) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +func luaMainModuleMatchShellFile(L *lua.LState) int { + longest := L.CheckString(1) + + matches, longest := matchShellFile(longest) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +func luaMainModuleMatchExec(L *lua.LState) int { + longest := L.CheckString(1) + + matches, longest := matchExec(longest) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +func luaMainModuleMatchSearch(L *lua.LState) int { + longest := L.CheckString(1) + + matches, longest := matchSearch(longest) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +func luaMainModuleMatchOptName(L *lua.LState) int { + longest := L.CheckString(1) + + matches, longest := matchWord(longest, gOptWords) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +func luaMainModuleMatchLocalOptName(L *lua.LState) int { + longest := L.CheckString(1) + + matches, longest := matchWord(longest, gLocalOptWords) + + return luaMainModuleAddMatchResultToLuaState(L, matches, longest) +} + +func luaMainModuleStrFill(L *lua.LState) int { + base := L.CheckString(1) + width := L.CheckInt(2) + fillStrValue := L.Get(3) + + fillStr := " " + switch fillStrValue.Type() { + case lua.LTString: + fillStr = string(fillStrValue.(lua.LString)) + case lua.LTNil: + // pass + default: + L.ArgError(3, "a string is expected") + } + + baseLen := len(base) + fillLen := len(fillStr) + repeatCnt := (width - baseLen) / fillLen + if repeatCnt <= 0 { + L.Push(lua.LString(base)) + return 1 + } + + L.Push(lua.LString(strings.Repeat(fillStr, repeatCnt) + base)) + + return 1 +} + +func luaMainModuleStrFillRight(L *lua.LState) int { + base := L.CheckString(1) + width := L.CheckInt(2) + fillStrValue := L.Get(3) + + fillStr := " " + switch fillStrValue.Type() { + case lua.LTString: + fillStr = string(fillStrValue.(lua.LString)) + case lua.LTNil: + // pass + default: + L.ArgError(3, "a string is expected") + } + + baseLen := len(base) + fillLen := len(fillStr) + repeatCnt := (width - baseLen) / fillLen + if repeatCnt <= 0 { + L.Push(lua.LString(base)) + return 1 + } + + L.Push(lua.LString(base + strings.Repeat(fillStr, repeatCnt))) + + return 1 +} + +func luaMainModuleToPermString(L *lua.LState) int { + mod := lCheckFileMode(L, 1) + L.Push(lua.LString(permString(mod))) + return 1 +} + +func luaMainModuleMakeLinkCountStr(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LString(linkCount(file))) + return 1 +} + +func luaMainModuleMakeUserNameStr(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LString(userName(file))) + return 1 +} + +func luaMainModuleMakeGroupNameStr(L *lua.LState) int { + file := lCheckFile(L, 1) + L.Push(lua.LString(groupName(file))) + return 1 +} + +func luaMainModuleSanitizeName(L *lua.LState) int { + str := L.CheckString(1) + L.Push(lua.LString(sanitizeName(str))) + return 1 +} + +func luaMainModuleFileSizeHumanize(L *lua.LState) int { + size := L.CheckInt64(1) + L.Push(lua.LString(humanize(size))) + return 1 +} + +func luaMainModuleDiskFreeSpace(L *lua.LState) int { + pathValue := L.Get(1) + + path := "." + switch pathValue.Type() { + case lua.LTString: + path = string(pathValue.(lua.LString)) + case lua.LTNil: + // pass + default: + L.ArgError(1, "string expected") + } + + L.Push(lua.LString(diskFree(path))) + + return 1 +} diff --git a/lua_module_ui.go b/lua_module_ui.go new file mode 100644 index 00000000..7c3f38ba --- /dev/null +++ b/lua_module_ui.go @@ -0,0 +1,222 @@ +package main + +import ( + "fmt" + + "github.com/clipperhouse/displaywidth" + lua "github.com/yuin/gopher-lua" +) + +func lfUIModuleLoader(L *lua.LState) int { + mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "print_length": lfUIModulePrintLength, + "display_width": lfUIModuleDisplayWidth, + + "get_formatter": luaUIModuleGetUIFormatter, + "get_printer": luaUIModuleGetUIPrinter, + "call_formatter_with_default_str": luaUIModuleCallFormatterWithDefaultStr, + "get_style_with_default_str": luaUIModuleGetStyleWithDefaultStr, + "format_option_str": luaUIModuleFormatUIOptionStr, + "get_file_display_info": luaUIModuleGetFileDisplayInfo, + "truncate_filename": luaUIModuleTruncateFilename, + "option_to_fmtstr": luaUIModuleOptionToFmtstr, + "strip_term_sequence": luaUIModuleStripTermSequence, + + "print_dir_entry_list": luaUIModulePrintDirEntryList, + "print_dir_entry_plain": luaUIModulePrintDirEntryPlain, + "print_directory_plain": luaUIModulePrintDirPlain, + }) + + L.Push(mod) + + return 1 +} + +// lfUIModulePrintLength returns displayed width of string content in terminal cells. +// +// It ignores supported terminal control sequences and accounts for tab +// expansions using the `tabstop` option. +func lfUIModulePrintLength(L *lua.LState) int { + str := L.CheckString(1) + L.Push(lua.LNumber(printLength(str))) + return 1 +} + +// lfUIModuleDisplayWidth calculates the display width of a string, by iterating +// over grapheme clusters in the string and summing their widths. +func lfUIModuleDisplayWidth(L *lua.LState) int { + str := L.CheckString(1) + L.Push(lua.LNumber(displaywidth.String(str))) + return 1 +} + +func luaUIModuleGetUIFormatter(L *lua.LState) int { + name := L.CheckString(1) + formatter := getLuaUIFormatter(name) + return lAddLuaMsgExprToState(L, formatter) +} + +func luaUIModuleGetUIPrinter(L *lua.LState) int { + name := L.CheckString(1) + expr := getLuaUIPrinter(name) + return lAddLuaMsgExprToState(L, expr) +} + +func luaUIModuleCallFormatterWithDefaultStr(L *lua.LState) int { + name := L.CheckString(1) + defaultFmtStr := L.CheckString(2) + nArgs := L.GetTop() + + expr := getLuaUIFormatter(name) + if expr == nil { + offset := 3 + param := make([]any, nArgs-offset+1) + for i := offset; i <= nArgs; i++ { + param[i-offset] = L.Get(i).String() + } + + result := fmt.Sprintf(optionToFmtstr(defaultFmtStr), param...) + L.Push(lua.LString(result)) + + return 1 + } + + action, err := getLuaMsgAction(L, expr.sourceName, expr.registry, expr.msg, expr.variant) + if err != nil { + L.RaiseError("%s", err) + return 0 + } + + L.Replace(1, action) + for i := 2; i <= nArgs; i++ { + L.Replace(i, L.Get(i+1)) + } + L.Call(L.GetTop()-1, lua.MultRet) + + return L.GetTop() +} + +func luaUIModuleGetStyleWithDefaultStr(L *lua.LState) int { + name := L.CheckString(1) + defaultFmtStr := L.CheckString(2) + st := getLuaUIStyleWithDefaultStr(name, defaultFmtStr) + return lAddTcellStyleToState(L, &st) +} + +func luaUIModuleFormatUIOptionStr(L *lua.LState) int { + fmtStr := L.CheckString(1) + + offset := 2 + nArg := L.GetTop() + param := make([]any, nArg-offset+1) + for i := offset; i <= nArg; i++ { + param[i-offset] = L.Get(i).String() + } + + result := fmt.Sprintf(optionToFmtstr(fmtStr), param...) + L.Push(lua.LString(result)) + + return 1 +} + +func luaUIModuleGetFileDisplayInfo(L *lua.LState) int { + file := lCheckFile(L, 1) + dir := lCheckDir(L, 2) + userWidth := L.CheckInt(3) + groupWidth := L.CheckInt(4) + customWidth := L.CheckInt(5) + + info, custom, customOff := fileInfo(file, dir, userWidth, groupWidth, customWidth) + L.Push(lua.LString(info)) + L.Push(lua.LString(custom)) + L.Push(lua.LNumber(customOff)) + + return 3 +} + +func luaUIModuleTruncateFilename(L *lua.LState) int { + file := lCheckFile(L, 1) + maxWidth := L.CheckInt(2) + filename := truncateFilename(file, maxWidth, gOpts.truncatepct, gOpts.truncatechar) + L.Push(lua.LString(filename)) + return 1 +} + +func luaUIModuleOptionToFmtstr(L *lua.LState) int { + str := L.CheckString(1) + L.Push(lua.LString(optionToFmtstr(str))) + return 1 +} + +func luaUIModuleStripTermSequence(L *lua.LState) int { + str := L.CheckString(1) + L.Push(lua.LString(stripTermSequence(str))) + return 1 +} + +// luaUIModulePrintDirEntryList is default implementation of printing given list +// of directory entries. +func luaUIModulePrintDirEntryList(L *lua.LState) int { + tryRaiseSyncLuaStateError(L) + + win := lCheckWin(L, 1) + ui := lCheckUI(L, 2) + context := lCheckPrintDirEntryContext(L, 3) + fileTbl := L.CheckTable(4) + + nFile := fileTbl.Len() + files := make([]*file, nFile) + for i := 1; i <= nFile; i++ { + value := fileTbl.RawGetInt(i) + if ud, ok := value.(*lua.LUserData); ok { + if file, ok := ud.Value.(*file); ok { + files[i-1] = file + } else { + L.ArgError(4, fmt.Sprintf("element #%d is not a file object", i)) + } + } else { + L.ArgError(4, fmt.Sprintf("element #%d is not userdata", i)) + } + } + + if !tryPrintDirEntriesWithLua(win, ui, context, files) { + for i, f := range files { + printDirEntry(win, ui.screen, context, i, f) + } + } + + return 0 +} + +// luaUIModulePrintDirEntryPlain is default implementation of printing a single file +// onto screen without invoking `dir_entry` Lua UI printer. +func luaUIModulePrintDirEntryPlain(L *lua.LState) int { + tryRaiseSyncLuaStateError(L) + + win := lCheckWin(L, 1) + screen := lCheckTcellScreen(L, 2) + context := lCheckPrintDirEntryContext(L, 3) + index := L.CheckInt(4) + file := lCheckFile(L, 5) + + printDirEntry(win, screen, context, index, file) + + return 0 +} + +// luaUIModulePrintDirPlain is default implementaion of printing directory content +// onto screen without invoking `directory` Lua UI printer. +func luaUIModulePrintDirPlain(L *lua.LState) int { + tryRaiseSyncLuaStateError(L) + + win := lCheckWin(L, 1) + ui := lCheckUI(L, 2) + dir := lCheckDir(L, 3) + context := lCheckDirContext(L, 4) + dirStyle := lCheckDirStyle(L, 5) + previewTimer := lCheckTimer(L, 6) + + win.printDir(ui, dir, context, dirStyle, previewTimer) + + return 0 +} diff --git a/lua_module_utf8.go b/lua_module_utf8.go new file mode 100644 index 00000000..a7575ff5 --- /dev/null +++ b/lua_module_utf8.go @@ -0,0 +1,54 @@ +package main + +import ( + "unicode/utf8" + + lua "github.com/yuin/gopher-lua" +) + +func lfUtf8ModuleLoader(L *lua.LState) int { + mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "to_rune_tbl": luaUtf8ModuleToRuneTbl, + "len": luaUtf8ModuleLen, + "get_rune": luaUtf8ModuleGetRune, + }) + + L.Push(mod) + + return 1 +} + +// luaUtf8ModuleToRuneTbl converts given string into a list of UTF-8 runes. +func luaUtf8ModuleToRuneTbl(L *lua.LState) int { + str := L.CheckString(1) + + tbl := L.NewTable() + for _, r := range str { + tbl.Append(lua.LString(string(r))) + } + + L.Push(tbl) + + return 1 +} + +// luaUtf8ModuleLen returns length of a string counted in UTF-8 rune. +func luaUtf8ModuleLen(L *lua.LState) int { + str := L.CheckString(1) + length := utf8.RuneCountInString(str) + + L.Push(lua.LNumber(length)) + + return 1 +} + +// luaUtf8ModuleGetRune returns UTF-8 rune at given index. +func luaUtf8ModuleGetRune(L *lua.LState) int { + str := L.CheckString(1) + index := L.CheckInt(2) + + runes := []rune(str) + L.Push(lua.LString(string(runes[index-1]))) + + return 1 +} diff --git a/lua_plugin.go b/lua_plugin.go new file mode 100644 index 00000000..6e43637f --- /dev/null +++ b/lua_plugin.go @@ -0,0 +1,2682 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "log" + "maps" + "os" + "os/exec" + "path/filepath" + "reflect" + "slices" + "strings" + "sync" + "time" + + "github.com/gdamore/tcell/v3" + lua "github.com/yuin/gopher-lua" + "github.com/yuin/gopher-lua/parse" +) + +const luaPluginDirName = "plugins" + +const luaGlobalNameApp = "app" +const luaGlobalNameDataStore = "data_store" + +const luaMsgVariantMain = "" +const luaMsgMetaKeyIsAsync = "is_async" + +const ( + registryKeyCommand = "command" + registryKeyEventHook = "event_hook" + registryKeyKeyMap = "key_map" + registryKeyLocalOption = "local_option" + registryKeyMisc = "misc" + registryKeyOption = "option" + registryKeyPreviewer = "previewer" + registryKeySortingMethod = "sorting_method" + registryKeyUIFormatter = "ui_formatter" + registryKeyUIPrinter = "ui_printer" + registryKeyUIStyle = "ui_style" +) + +const ( + luaMiscMsgShell = "shell" + luaMiscMsgDupFile = "dupfile" + + luaUIFormatterCursorActive = "cursoractive" + luaUIFormatterCursorParent = "cursorparent" + luaUIFormatterCursorPreview = "cursorpreview" + luaUIFormatterError = "error" + luaUIFormatterNumberCursor = "numbercursor" + luaUIFormatterNumber = "number" + luaUIFormatterTag = "tag" + + luaUIPrinterDirEntry = "dir_entry" + luaUIPrinterDirectory = "directory" + luaUIPrinterRuler = "ruler" + luaUIPrinterPrompt = "prompt" + + luaUIStyleBorder = "border" + luaUIStyleCopy = "copy" + luaUIStyleCut = "cut" + luaUIStyleMenu = "menu" + luaUIStyleMenuheader = "menuheader" + luaUIStyleMenuselect = "menuselect" + luaUIStyleSelect = "select" + luaUIStyleVisual = "visual" +) + +const ( + luaCommandActionFuncKey = "action" + luaCommandCompletionFuncKey = "completion" + + luaEventHookActionFuncKey = "action" + + luaMiscActionFuncKey = "action" + + luaKeyMapActionFuncKey = "action" + + luaPreviewerActionFuncKey = "action" + luaPreviewerCleanFuncKey = "clean" + luaPreviewerConditionFuncKey = "condition" + + luaSortingMethodActionFuncKey = "action" + + luaUIFormatterActionFuncKey = "action" + + luaUIPrinterActionFuncKey = "action" +) + +const ( + luaKeyMapTypeNormal = "n" + luaKeyMapTypeVisual = "v" + luaKeyMapTypeCommand = "c" +) + +type luaDataStore struct { + lock sync.RWMutex + dataStore map[string]any +} + +func (store *luaDataStore) set(key string, value lua.LValue) error { + store.lock.Lock() + defer store.lock.Unlock() + + if store.dataStore == nil { + store.dataStore = make(map[string]any) + } + + if value == lua.LNil { + delete(store.dataStore, key) + return nil + } + + goValue, err := luaPlainValueToGoValue(value) + if err != nil { + return err + } + + store.dataStore[key] = goValue + + return nil +} + +func (store *luaDataStore) get(L *lua.LState, key string) (lua.LValue, error) { + store.lock.RLock() + defer store.lock.RUnlock() + + if store.dataStore == nil { + return lua.LNil, nil + } + + goValue := store.dataStore[key] + value, err := goValueToLuaValue(L, goValue) + if err != nil { + return lua.LNil, err + } + + return value, nil +} + +func (store *luaDataStore) clear() { + store.lock.Lock() + defer store.lock.Unlock() + + clear(store.dataStore) +} + +func (store *luaDataStore) keysAsLuaTbl(L *lua.LState) *lua.LTable { + store.lock.RLock() + defer store.lock.RUnlock() + + tbl := L.NewTable() + for k := range store.dataStore { + tbl.Append(lua.LString(k)) + } + + return tbl +} + +type lStatePool struct { + lockPool sync.Mutex + saved []*lua.LState + allStates []*lua.LState + + lockLuaStateSync sync.RWMutex + luaStateSync *lua.LState + + isInitialized bool // Lua global registry has been updated by instanciate first Lua state + isClosed bool // indicating a shutdown has been called on the pool + + app *app + pluginRootDirs []string // plugin root directory list + pluginByteCodes []*lua.FunctionProto // compiled Lua byte code of all plugin + + dataStore *luaDataStore +} + +func newLStatePool(app *app) *lStatePool { + return &lStatePool{ + app: app, + dataStore: new(luaDataStore), + } +} + +// ---------------------------------------------------------------------------- +// lock-free APIs + +// addPluginRoot adds given path to plugin root directory list. +func (pl *lStatePool) addPluginRoot(rootDir string) { + if !slices.Contains(pl.pluginRootDirs, rootDir) { + pl.pluginRootDirs = append(pl.pluginRootDirs, rootDir) + } +} + +// loadPluginScripts compiles and sotre plugin entrance Lua script founded under +// each plugin roots. +func (pl *lStatePool) loadPluginScripts() error { + pluginByteCodes := make([]*lua.FunctionProto, 0) + + errorCnt := 0 + + for _, pluginDir := range pl.pluginRootDirs { + if _, err := os.Stat(pluginDir); os.IsNotExist(err) { + continue + } + + entries, err := os.ReadDir(pluginDir) + if err != nil { + errorCnt++ + log.Printf("failed to read plugin directory %s: %s", pluginDir, err) + continue + } + + // only directories are treated as plugin entrance. + // So that user can put Lua development config files under plugin root with ease. + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + name := entry.Name() + scriptPath := filepath.Join(pluginDir, name, "init.lua") + + if _, err := os.Stat(scriptPath); !os.IsNotExist(err) { + proto, err := compileLua(scriptPath) + if err != nil { + errorCnt++ + log.Printf("failed to compile plugin script: %s\n%s", scriptPath, err) + } else { + log.Printf("plugin script loaded: %s", scriptPath) + pluginByteCodes = append(pluginByteCodes, proto) + } + } + } + } + + if errorCnt > 0 { + return fmt.Errorf("%d error(s) occured while loading plugin script, see log for details", errorCnt) + } + + pl.pluginByteCodes = pluginByteCodes + + return nil +} + +// newWithRetAction creates a new Lua state and takes a `action` function that +// can do extra stuff with value returned by each plugin script. +func (pl *lStatePool) newWithRetAction(action func(sourceName string, L *lua.LState, tbl *lua.LTable)) (*lua.LState, error) { + if pl.isClosed { + return nil, fmt.Errorf("pool has been closed on app quit") + } + + L := lua.NewState() + + if err := setupScripImportPath(L, pl.pluginRootDirs); err != nil { + log.Printf("failed to setup Lua loader search path: %s", err) + } + setupLuaGlobals(pl, L) + setupPreloadModules(L) + + for _, proto := range pl.pluginByteCodes { + err := doCompiledFile(L, proto) + if err != nil { + log.Printf("failed to execute plugin script: %s\n%s", proto.SourceName, err) + } + + ret := L.Get(1) + nRet := L.GetTop() + L.Pop(nRet) + + if ret.Type() == lua.LTNil { + if action != nil { + action(proto.SourceName, L, nil) + } + } else if ret.Type() == lua.LTTable { + sourceName := proto.SourceName + tbl := ret.(*lua.LTable) + + if gLuaRegistry.stateDataMap == nil { + gLuaRegistry.stateDataMap = make(map[*lua.LState]map[string]*lua.LTable) + } + + dataTblMap, ok := gLuaRegistry.stateDataMap[L] + if !ok { + dataTblMap = make(map[string]*lua.LTable) + gLuaRegistry.stateDataMap[L] = dataTblMap + } + + dataTblMap[sourceName] = tbl + + if action != nil { + action(sourceName, L, tbl) + } + } else { + log.Println("plugin script", proto.SourceName, "did not return a table") + } + } + + return L, nil +} + +// new creates a new Lua state. +func (pl *lStatePool) newSimple() (*lua.LState, error) { + return pl.newWithRetAction(nil) +} + +// newWithRegistryUpdate creates a new Lua state, and updates global Lua registry +// with value returned by each plugin script during Lua state initialization. +// +// P.S.: this function modify global option and registry, this function should +// be called on main goroutine. +func (pl *lStatePool) newWithRegistryUpdate() (*lua.LState, error) { + return pl.newWithRetAction(func(sourceName string, L *lua.LState, tbl *lua.LTable) { + if tbl == nil { + return + } + + log.Println("update Lua registry for plugin script:", sourceName) + + loadCommandRegistryFromTbl(sourceName, tbl) + loadEventHookRegistryFromTbl(sourceName, tbl) + loadKeyMapRegistryFromTbl(sourceName, tbl) + loadPreviewerRegistryFromTbl(sourceName, tbl) + loadMiscRegistryFromTbl(sourceName, tbl) + loadSortingMethodRegistryFromTbl(sourceName, tbl) + loadUIFormatterRegistryFromTbl(sourceName, tbl) + loadUIPrinterRegistryFromTbl(sourceName, tbl) + loadUIStyleRegistryFromTbl(L, sourceName, tbl) + + sortLuaPreviewers() + }) +} + +// ---------------------------------------------------------------------------- + +// initializeState loads plugin script byte code and create synchronous Lua state +// object. +func (pl *lStatePool) initializeState(app *app) { + pl.lockPool.Lock() + defer pl.lockPool.Unlock() + + if pl.isInitialized { + return + } + + pl.isInitialized = true + + err := gLuaPool.loadPluginScripts() + if err != nil { + app.ui.echoerr(err.Error()) + } + + // initialize sycnhronous Lua state and Lua registry + if L, err := gLuaPool.newWithRegistryUpdate(); err == nil { + pl.luaStateSync = L + } else { + app.ui.echoerrf("failed to initialize synchronous Lua state: %s", err) + } +} + +// get takes one Lua state from pool. +func (pl *lStatePool) get() (*lua.LState, error) { + pl.lockPool.Lock() + defer pl.lockPool.Unlock() + + if !pl.isInitialized { + return nil, fmt.Errorf("lua state has not been initialized") + } + + if pl.isClosed { + return nil, fmt.Errorf("lua state has been closed on app quit") + } + + n := len(pl.saved) + if n > 0 { + L := pl.saved[n-1] + pl.saved = pl.saved[0 : n-1] + return L, nil + } + + L, err := pl.newSimple() + if L != nil { + pl.allStates = append(pl.allStates, L) + } + + return L, err +} + +// put returns a Lua state to pool. +func (pl *lStatePool) put(L *lua.LState) { + pl.lockPool.Lock() + defer pl.lockPool.Unlock() + + if pl.isClosed { + L.Close() + } + pl.saved = append(pl.saved, L) +} + +// acquireSyncState tries acquire synchronous Lua state's mutex, and returns +// synchronous Lua state object after successfully acquired lock. +func (pl *lStatePool) acquireSyncState() (*lua.LState, error) { + pl.lockPool.Lock() + + if !pl.isInitialized { + pl.lockPool.Unlock() + return nil, fmt.Errorf("pool has not been initialized") + } + + if pl.isClosed { + pl.lockPool.Unlock() + return nil, fmt.Errorf("pool has been closed on app quit") + } + + pl.lockPool.Unlock() + + pl.lockLuaStateSync.Lock() + + if pl.luaStateSync == nil { + return nil, fmt.Errorf("no Lua State is available") + } + + return pl.luaStateSync, nil +} + +// releaseSyncState releases synchronous Lua state's mutex. +func (pl *lStatePool) releaseSyncState() { + pl.lockLuaStateSync.Unlock() +} + +// shutdown closes all Lua states in pool. +func (pl *lStatePool) shutdown() { + pl.lockPool.Lock() + defer pl.lockPool.Unlock() + + pl.lockLuaStateSync.Lock() + defer pl.lockLuaStateSync.Unlock() + + pl.isClosed = true + + if pl.luaStateSync != nil { + pl.luaStateSync.Close() + } + + for _, L := range pl.saved { + L.Close() + } +} + +// resetLuaState closes and removes all Lua state in pool object, and reset Lua +// related data to uninitialized state. +func (pl *lStatePool) resetLuaState() error { + pl.lockLuaStateSync.Lock() + defer pl.lockLuaStateSync.Unlock() + + for range 10 { + pl.lockPool.Lock() + + if len(pl.saved) != len(pl.allStates) { + // some Lua states are occupied + pl.lockPool.Unlock() + <-time.After(10 * time.Millisecond) + continue + } + + if pl.luaStateSync != nil { + pl.luaStateSync.Close() + } + + for _, L := range pl.allStates { + L.Close() + } + + pl.saved = nil + pl.allStates = nil + pl.luaStateSync = nil + + pl.isInitialized = false + + pl.pluginByteCodes = nil + + pl.lockPool.Unlock() + + return nil + } + + return fmt.Errorf("lua State is busy") +} + +// checkIsSyncState checks if given state is synchronous Lua state. +func (pl *lStatePool) checkIsSyncState(L *lua.LState) bool { + return pl.luaStateSync == L +} + +type luaPreviewerInfo struct { + priority int // priority value for this previewer + name string // name of this previewer, takes the form `.` + hasCleaner bool // if a cleaner function is defined for this previewer + msgexpr luaMsgExpr +} + +// luaFuncWriter implments io.Writer interface with a Lua function. +type luaFuncWriter struct { + luaState *lua.LState + fn *lua.LFunction +} + +func (writer *luaFuncWriter) Write(p []byte) (n int, err error) { + luaErr := writer.luaState.CallByParam( + lua.P{ + Fn: writer.fn, + NRet: 2, + Protect: true, + }, + lua.LString(string(p)), + ) + if luaErr != nil { + return 0, luaErr + } + + defer writer.luaState.Pop(2) + + ret1 := writer.luaState.Get(-2) + if ret1.Type() != lua.LTNumber { + return 0, fmt.Errorf("return value #1 of Lua write function is not a number") + } + n = int(ret1.(lua.LNumber)) + + var errStr string + ret2 := writer.luaState.Get(-1) + if lua.LVAsBool(ret2) { + errStr = ret2.String() + } + + if errStr != "" { + err = fmt.Errorf("lua writer function error: %s", err) + } + + return +} + +// ---------------------------------------------------------------------------- +// Lua registry value operation + +// Global LState pool, used for asynchronous execution +var gLuaPool *lStatePool + +var gLuaRegistry struct { + stateDataMap map[*lua.LState]map[string]*lua.LTable + + eventHooks map[string][]*luaMsgExpr + misc map[string]*luaMsgExpr + previewers []luaPreviewerInfo + sortingMethod map[string]*luaMsgExpr + uiFormatter map[string]*luaMsgExpr + uiPrinter map[string]*luaMsgExpr + uiStyleMap map[string]tcell.Style +} + +func goReflectValueToLuaValue(L *lua.LState, rValue reflect.Value) (luaValue lua.LValue, err error) { + switch rValue.Kind() { + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64: + luaValue = lua.LNumber(rValue.Convert(reflect.TypeOf(float64(0))).Float()) + case reflect.String: + luaValue = lua.LString(rValue.String()) + case reflect.Bool: + if rValue.Bool() { + luaValue = lua.LTrue + } else { + luaValue = lua.LFalse + } + case reflect.Slice, reflect.Array: + var elemValue lua.LValue + tbl := L.NewTable() + + for i := 0; i < rValue.Len(); i++ { + elem := rValue.Index(i) + if elemValue, err = goReflectValueToLuaValue(L, elem); err == nil { + tbl.Append(elemValue) + } else { + err = fmt.Errorf("failed to convert slice/array element: %s", err) + break + } + } + + luaValue = tbl + case reflect.Map: + var lMapKey, lMapValue lua.LValue + tbl := L.NewTable() + keys := rValue.MapKeys() + + for _, mapKey := range keys { + mapValue := rValue.MapIndex(mapKey) + + lMapKey, err = goReflectValueToLuaValue(L, mapKey) + if err != nil { + err = fmt.Errorf("failed to convert map key: %s", err) + break + } + + lMapValue, err = goReflectValueToLuaValue(L, mapValue) + if err != nil { + err = fmt.Errorf("failed to convert map value: %s", err) + break + } + + tbl.RawSet(lMapKey, lMapValue) + } + + luaValue = tbl + case reflect.Pointer: + if rValue.IsNil() { + luaValue = lua.LNil + } else { + ud := L.NewUserData() + ud.Value = rValue.Pointer() + } + case reflect.Interface: + if rValue.IsNil() { + luaValue = lua.LNil + } else if rValue.CanInterface() { + ud := L.NewUserData() + ud.Value = rValue.Interface() + } else { + err = fmt.Errorf("unaccessable interface value") + } + case reflect.Struct: + if rValue.CanAddr() { + ud := L.NewUserData() + ud.Value = rValue.Addr().Pointer() + } else if rValue.CanInterface() { + ud := L.NewUserData() + ud.Value = rValue.Interface() + } else { + err = fmt.Errorf("unaddressable struct value") + } + default: + err = fmt.Errorf("unsupported value type: %s", rValue.Kind()) + luaValue = lua.LNil + } + + return +} + +// goValueToLuaValue converts Go value to lua.LValue. +func goValueToLuaValue(L *lua.LState, value any) (lua.LValue, error) { + var err error + + lValue, ok := value.(lua.LValue) + if ok { + return lValue, err + } + + return goReflectValueToLuaValue(L, reflect.ValueOf(value)) +} + +// luaPlainValueToGoValue converts simple Lua value to Go value. +func luaPlainValueToGoValue(value lua.LValue) (any, error) { + switch value.Type() { + case lua.LTNil: + return nil, nil + case lua.LTBool: + if value == lua.LTrue { + return true, nil + } else { + return false, nil + } + case lua.LTNumber: + return float64(value.(lua.LNumber)), nil + case lua.LTString: + return string(value.(lua.LString)), nil + default: + return nil, fmt.Errorf("unsupported type: %s", value.Type()) + } +} + +// getAppObjectFromLuaGlobals fetchs app object from Lua state's global variable. +func getAppObjectFromLuaGlobals(L *lua.LState) (*app, error) { + value := L.GetGlobal(luaGlobalNameApp) + + ud, ok := value.(*lua.LUserData) + if !ok { + return nil, fmt.Errorf("global variable `%s` is not a user data", luaGlobalNameApp) + } + + app, ok := ud.Value.(*app) + if !ok { + return nil, fmt.Errorf("global variable `%s` is not `*app` value", ud.Value) + } + + return app, nil +} + +// getPluginNameForSourcePath return plugin name for given plugin script path +func getPluginNameForSourcePath(sourceName string) string { + return filepath.Base(filepath.Dir(sourceName)) +} + +// compileLua reads the passed lua file from disk and compiles it. +func compileLua(filePath string) (*lua.FunctionProto, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + + defer file.Close() + if err != nil { + return nil, err + } + reader := bufio.NewReader(file) + chunk, err := parse.Parse(reader, filePath) + if err != nil { + return nil, err + } + proto, err := lua.Compile(chunk, filePath) + if err != nil { + return nil, err + } + return proto, nil +} + +// doCompiledFile takes a FunctionProto, as returned by CompileLua, and runs it in the LState. It is equivalent +// to calling DoFile on the LState with the original source file. +func doCompiledFile(L *lua.LState, proto *lua.FunctionProto) error { + lfunc := L.NewFunctionFromProto(proto) + L.Push(lfunc) + return L.PCall(0, lua.MultRet, nil) +} + +// setupScripImportPath appends plugin root directory paths to Lua loader search +// list. +func setupScripImportPath(L *lua.LState, runtimeDirs []string) error { + pack, ok := L.GetGlobal("package").(*lua.LTable) + if !ok { + return fmt.Errorf("failed to retrive global variable `package`") + } + + pathVal, ok := L.GetField(pack, "path").(lua.LString) + if !ok { + return fmt.Errorf("`path` field of `package` table is not a string") + } + + path := string(pathVal) + + var builder strings.Builder + builder.WriteString(path) + for _, dir := range runtimeDirs { + builder.WriteString(";") + builder.WriteString(dir) + builder.WriteString("/?.lua") + + builder.WriteString(";") + builder.WriteString(dir) + builder.WriteString("/?/init.lua") + } + + path = builder.String() + + L.SetField(pack, "path", lua.LString(path)) + + return nil +} + +// setupLuaGlobals setup global variables. +func setupLuaGlobals(pl *lStatePool, L *lua.LState) { + // Lua meta table registering must happens before any user data gets pushed + // onto Lua state. + setupLuaTypeBindings(L) + + L.SetGlobal("print", L.NewFunction(func(L *lua.LState) int { + nargs := L.GetTop() + values := make([]any, nargs) + + for i := range nargs { + value := L.Get(i + 1) + values[i] = value.String() + } + + log.Println(values...) + + return 0 + })) + + L.SetGlobal(luaGlobalNameApp, lWrapApp(L, pl.app)) + L.SetGlobal(luaGlobalNameDataStore, lWrapLuaDataStore(L, pl.dataStore)) +} + +// setupLuaTypeBindings adds `lf_types` global table as entrance of accessing +// Go type binding meta tables. +func setupLuaTypeBindings(L *lua.LState) { + lfTypes := L.NewTable() + + // bufio + lfTypes.RawSetString("BufWriter", lRegisterBufWriterType(L)) + lfTypes.RawSetString("BufReader", lRegisterBufReaderType(L)) + // exec + lfTypes.RawSetString("Cmd", lRegisterCmdType(L)) + // fs + lfTypes.RawSetString("DirEntry", lRegisterDirEntryType(L)) + lfTypes.RawSetString("FileInfo", lRegisterFileInfoType(L)) + lfTypes.RawSetString("FileMode", lRegisterFileModeType(L)) + // main + lfTypes.RawSetString("App", lRegisterAppType(L)) + lfTypes.RawSetString("CompMatch", lRegisterCompMatchType(L)) + lfTypes.RawSetString("Clipboard", lRegisterClipboardType(L)) + lfTypes.RawSetString("Dir", lRegisterDirType(L)) + lfTypes.RawSetString("DirContext", lRegisterDirContextType(L)) + lfTypes.RawSetString("DirStyle", lRegisterDirStyleType(L)) + lfTypes.RawSetString("File", lRegisterFileTypeMt(L)) + lfTypes.RawSetString("FuncWriter", lRegisterFuncWriterType(L)) + lfTypes.RawSetString("IconDef", lRegisterIconDefType(L)) + lfTypes.RawSetString("IconMap", lRegisterIconMapType(L)) + lfTypes.RawSetString("LuaDataStore", lRegisterLuaDataStoreType(L)) + lfTypes.RawSetString("LuaMsgExpr", lRegisterLuaMsgExprType(L)) + lfTypes.RawSetString("Nav", lRegisterNavType(L)) + lfTypes.RawSetString("PrintDirEntryContext", lRegisterPrintDirEntryContextType(L)) + lfTypes.RawSetString("StyleMap", lRegisterStyleMapType(L)) + lfTypes.RawSetString("Win", lRegisterWinType(L)) + lfTypes.RawSetString("UI", lRegisterUIType(L)) + // tcell + lfTypes.RawSetString("TcellColor", lRegisterTcellColorType(L)) + lfTypes.RawSetString("TcellScreen", lRegisterTcellScreenType(L)) + lfTypes.RawSetString("TcellStyle", lRegisterTcellStyleType(L)) + // time + lfTypes.RawSetString("Duration", lRegisterDurationType(L)) + lfTypes.RawSetString("Month", lRegisterMonthType(L)) + lfTypes.RawSetString("Time", lRegisterTimeType(L)) + lfTypes.RawSetString("Timer", lRegisterTimerType(L)) + lfTypes.RawSetString("Weekday", lRegisterWeekdayType(L)) + + L.SetGlobal("lf_types", lfTypes) +} + +// setupPreloadModules register load functions for preload modules. +func setupPreloadModules(L *lua.LState) { + L.PreloadModule("lf", lfMainModuleLoader) + L.PreloadModule("lf.fs", lfFsModuleLoader) + L.PreloadModule("lf.utf8", lfUtf8ModuleLoader) + L.PreloadModule("lf.ui", lfUIModuleLoader) +} + +// tryRaiseNonSyncLuaStateError raises an error if `L` is not synchronous Lua state. +// This function is used to enforce a Lua API to be called on synchronous Lua state. +func tryRaiseNonSyncLuaStateError(L *lua.LState) { + if !gLuaPool.checkIsSyncState(L) { + app, _ := getAppObjectFromLuaGlobals(L) + if app != nil { + app.ui.exprChan <- &callExpr{"echoerr", []string{"synchronous Lua function is called under async mode"}, 1} + } + L.RaiseError("this func should be called with synchronous mode") + } +} + +// tryRaiseSyncLuaStateError raises an error if `L` is synchronous Lua state. +// This function is used to enforce a Lua API to be called with asynchronous mode, +// such as a Lua API that calls other Lua mesages in it. +func tryRaiseSyncLuaStateError(L *lua.LState) { + if gLuaPool.checkIsSyncState(L) { + app, _ := getAppObjectFromLuaGlobals(L) + if app != nil { + app.ui.exprChan <- &callExpr{"echoerr", []string{"async Lua API is called under synchronous mode"}, 1} + } + L.RaiseError("this func should be called with asynchronous mode") + } +} + +// loadLuaPluginOptionValue loads option and local option defined in Lua script. +// This process may trigger other Lua message call, it has to be seperated from +// other registry load. +func loadLuaPluginOptionValue(app *app) { + L, err := gLuaPool.get() + defer gLuaPool.put(L) + + if err != nil { + app.ui.echoerrf("failed to initialize async Lua state") + return + } + + registryMap, ok := gLuaRegistry.stateDataMap[L] + if ok { + for _, tbl := range registryMap { + loadLocalOptionRegistryFromTbl(L, app, tbl) + loadOptionRegistryFromTbl(L, app, tbl) + } + } +} + +// initializeLua load plugin scripts, and initialize Lua state. +// +// P.S.: this function modify global option and registry, this function should +// be called on main goroutine. +func initializeLua(app *app) { + gLuaPool = newLStatePool(app) + + if gPluginDir != "" { + gLuaPool.addPluginRoot(gPluginDir) + } else if gConfigPath != "" { + pluginRoot := filepath.Join(filepath.Dir(gConfigPath), luaPluginDirName) + gLuaPool.addPluginRoot(pluginRoot) + } else { + for _, path := range gConfigPaths { + pluginRoot := filepath.Join(filepath.Dir(path), luaPluginDirName) + gLuaPool.addPluginRoot(pluginRoot) + } + } + + gLuaPool.initializeState(app) + loadLuaPluginOptionValue(app) +} + +// luaPluginReload reset Lua state, Lua registry, then reload Lua script again. +// +// P.S.: this function modify global option and registry, this function should +// be called on main goroutine. +func luaPluginReload(app *app) { + err := gLuaPool.resetLuaState() + if err != nil { + app.ui.echoerrf("Lua plugin reload failed: %s", err) + return + } + + gLuaRegistry.stateDataMap = make(map[*lua.LState]map[string]*lua.LTable) + + gLuaRegistry.sortingMethod = make(map[string]*luaMsgExpr) + gLuaRegistry.eventHooks = make(map[string][]*luaMsgExpr) + gLuaRegistry.previewers = nil + gLuaRegistry.misc = make(map[string]*luaMsgExpr) + gLuaRegistry.uiFormatter = make(map[string]*luaMsgExpr) + gLuaRegistry.uiPrinter = make(map[string]*luaMsgExpr) + gLuaRegistry.uiStyleMap = make(map[string]tcell.Style) + + gLuaPool.initializeState(app) + loadLuaPluginOptionValue(app) + + app.ui.echo("Lua plugins reloaded") +} + +// ---------------------------------------------------------------------------- + +// loadCommandRegistryFromTbl registers commands defined in table returned from +// plugin script. +func loadCommandRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeyCommand + + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + cnt := 0 + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("command registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + msg := key.String() + + switch value.Type() { + case lua.LTString: + text := value.String() + p := newParser(strings.NewReader(text)) + expr := p.parseExpr() + if expr == nil { + log.Printf("failed to parse Lua command: %s", text) + return + } else { + gOpts.cmds[msg] = expr + } + case lua.LTFunction: + gOpts.cmds[msg] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + } + case lua.LTTable: + actionValue := value.(*lua.LTable).RawGetString(luaCommandActionFuncKey) + if actionValue.Type() == lua.LTFunction { + gOpts.cmds[msg] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: lua.LVAsBool(value.(*lua.LTable).RawGetString(luaMsgMetaKeyIsAsync)), + } + } else { + log.Printf("invalid command action value: %s", value) + return + } + default: + log.Printf("invalid command registry value of type %s: %s", value.Type(), value) + return + } + + cnt++ + }) + + if cnt > 0 { + log.Printf("%d command(s) added", cnt) + } +} + +// loadEventHookRegistryFromTbl registers event hooks defined in table returned +// from plugin script. +func loadEventHookRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeyEventHook + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + if gLuaRegistry.eventHooks == nil { + gLuaRegistry.eventHooks = make(map[string][]*luaMsgExpr) + } + + cnt := 0 + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("event hook registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + msg := key.String() + + switch value.Type() { + case lua.LTFunction: + gLuaRegistry.eventHooks[msg] = append( + gLuaRegistry.eventHooks[msg], + &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: false, + }, + ) + case lua.LTTable: + gLuaRegistry.eventHooks[msg] = append( + gLuaRegistry.eventHooks[msg], + &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: lua.LVAsBool(value.(*lua.LTable).RawGetString(luaMsgMetaKeyIsAsync)), + }, + ) + default: + log.Printf("unsupported event hook registry value type for key: %s", msg) + return + } + + cnt++ + }) + + if cnt > 0 { + log.Printf("%d event hook(s) added", cnt) + } +} + +// loadKeyMapRegistryFromTbl registers key maps defined in table returned from plugin +// script. +func loadKeyMapRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeyKeyMap + + value := tbl.RawGetString(registryKey) + switch value.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + registryTbl := value.(*lua.LTable) + + addKeyMapForRegistryValue(registryTbl, sourceName, luaKeyMapTypeNormal, "normal", gOpts.nkeys) + addKeyMapForRegistryValue(registryTbl, sourceName, luaKeyMapTypeVisual, "visual", gOpts.vkeys) + addKeyMapForRegistryValue(registryTbl, sourceName, luaKeyMapTypeCommand, "command", gOpts.cmdkeys) +} + +// addKeyMapForRegistryValue loads one type of key map from registry table. +func addKeyMapForRegistryValue(registryTbl *lua.LTable, sourceName, keyMapType, displayName string, keys map[string]expr) { + tbl := registryTbl.RawGetString(keyMapType) + switch tbl.Type() { + case lua.LTTable: + // ok + case lua.LTNil: + return + default: + log.Printf("key map group %s is not a table: %s", keyMapType, tbl) + return + } + + keyMapCnt := 0 + + tbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("map registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + mapKey := key.String() + isAsync := false + + mapAction := value + if value.Type() == lua.LTTable { + tbl := value.(*lua.LTable) + + mapAction = tbl.RawGetString(luaKeyMapActionFuncKey) + isAsync = lua.LVAsBool(tbl.RawGetString(luaMsgMetaKeyIsAsync)) + } + + switch mapAction.Type() { + case lua.LTString: + text := mapAction.String() + if text == "" { + delete(keys, mapKey) + } else { + p := newParser(strings.NewReader(text)) + expr := p.parseExpr() + if expr == nil { + log.Printf("failed to parse Lua key map %s.%s: %s", keyMapType, mapKey, p.err) + } else { + keys[mapKey] = expr + } + } + case lua.LTFunction: + keys[mapKey] = &luaKeyMapExpr{ + sourceName: sourceName, + keyMapType: keyMapType, + key: mapKey, + count: 1, + isAsync: isAsync, + } + default: + log.Printf("unsupported key map registry value for %s.%s", keyMapType, mapKey) + return + } + + keyMapCnt++ + }) + + if keyMapCnt > 0 { + log.Printf("%d %s key map(s) added", keyMapCnt, displayName) + } +} + +// loadLocalPreviewerRegistryFromTbl loads option value from table returned from plugin script. +func loadLocalOptionRegistryFromTbl(L *lua.LState, app *app, tbl *lua.LTable) { + registryKey := registryKeyLocalOption + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + registryTbl.(*lua.LTable).ForEach(func(pathKey, optionTbl lua.LValue) { + if pathKey.Type() != lua.LTString { + log.Printf("local option registry key is expected to be string, found %s: %s", pathKey.Type(), pathKey) + return + } + + path := string(pathKey.(lua.LString)) + + if optionTbl.Type() != lua.LTTable { + app.ui.echoerrf("registry value for local option group `%s` is not a table", path) + return + } + + optionTbl.(*lua.LTable).ForEach(func(optionKey, optionValue lua.LValue) { + if optionKey.Type() != lua.LTString { + app.ui.echoerrf("local option group option key is expected to be string, found %s: %s", optionKey.Type(), optionKey) + return + } + + option := string(optionKey.(lua.LString)) + + switch optionValue.Type() { + case lua.LTString: + expr := &setLocalExpr{path: path, opt: option, val: string(optionValue.(lua.LString))} + expr.eval(app, nil) + case lua.LTFunction: + err := L.CallByParam(lua.P{ + Fn: optionValue.(*lua.LFunction), + NRet: 1, + Protect: true, + }) + if err != nil { + app.ui.echoerrf("failed to evaluate local option `%s`, see log for more detail", option) + log.Printf("failed to run function for local option `%s` `%s`: %s", path, option, err) + } + + defer L.Pop(1) + ret := L.Get(-1) + if ret.Type() == lua.LTString { + expr := &setLocalExpr{path: path, opt: option, val: string(ret.(lua.LString))} + expr.eval(app, nil) + } else { + app.ui.echoerrf("lua function for local option `%s` `%s` does not return string value", path, option) + } + default: + log.Printf("unsupported local option registry value type for %s %s", path, option) + return + } + }) + }) +} + +// loadMiscRegistryFromTbl loads shell relative registry entry. +func loadMiscRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeyMisc + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + if gLuaRegistry.misc == nil { + gLuaRegistry.misc = make(map[string]*luaMsgExpr) + } + + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("misc registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + msg := key.String() + switch msg { + case luaMiscMsgDupFile, + luaMiscMsgShell: + // ok + default: + log.Println("unsupported misc registry entry key:", msg) + return + } + + switch value.Type() { + case lua.LTFunction: + gLuaRegistry.misc[msg] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: false, + } + case lua.LTTable: + gLuaRegistry.misc[msg] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: lua.LVAsBool(value.(*lua.LTable).RawGetString(luaMsgMetaKeyIsAsync)), + } + default: + log.Printf("unsupported misc registry value for key: %s", msg) + return + } + }) +} + +// loadPreviewerRegistryFromTbl loads option value from table returned from plugin script. +func loadOptionRegistryFromTbl(L *lua.LState, app *app, tbl *lua.LTable) { + registryKey := registryKeyOption + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("option registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + option := key.String() + + switch value.Type() { + case lua.LTString: + expr := &setExpr{opt: option, val: string(value.(lua.LString))} + expr.eval(app, nil) + case lua.LTFunction: + err := L.CallByParam(lua.P{ + Fn: value.(*lua.LFunction), + NRet: 1, + Protect: true, + }) + if err != nil { + app.ui.echoerrf("failed to evaluate option `%s`, see log for more detail", option) + log.Printf("failed to run function for option `%s`: %s", option, err) + } + + defer L.Pop(1) + ret := L.Get(-1) + if ret.Type() == lua.LTString { + expr := &setExpr{opt: option, val: string(ret.(lua.LString))} + expr.eval(app, nil) + } else { + app.ui.echoerrf("lua function for option `%s` does not return string value", option) + } + default: + log.Printf("unsupported option registry value type for key: %s", option) + return + } + }) +} + +// loadPreviewerRegistryFromTbl registers previewers defined in table returned +// rom plugin script. +func loadPreviewerRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeyPreviewer + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("previewer registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + msg := key.String() + name := getPluginNameForSourcePath(sourceName) + "." + msg + + switch value.Type() { + case lua.LTFunction: + gLuaRegistry.previewers = append(gLuaRegistry.previewers, luaPreviewerInfo{ + priority: 0, + name: name, + msgexpr: luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: false, + }, + }) + case lua.LTTable: + previewerTbl := value.(*lua.LTable) + hasCleaner := previewerTbl.RawGetString(luaPreviewerCleanFuncKey).Type() == lua.LTFunction + + gLuaRegistry.previewers = append(gLuaRegistry.previewers, luaPreviewerInfo{ + priority: 0, + name: name, + hasCleaner: hasCleaner, + msgexpr: luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: lua.LVAsBool(previewerTbl.RawGetString(luaMsgMetaKeyIsAsync)), + }, + }) + default: + log.Printf("unsupported previewer registry value type for key: %s", msg) + return + } + + log.Printf("add previewer: %s", name) + }) +} + +// sortLuaPreviewers sorts Lua previewers by their priority and name. Previewers +// with higher priority takes precedence. +func sortLuaPreviewers() { + slices.SortStableFunc(gLuaRegistry.previewers, func(a, b luaPreviewerInfo) int { + if a.priority < b.priority { + return 1 + } else if a.priority > b.priority { + return -1 + } + + if a.name < b.name { + return -1 + } else if a.name > b.name { + return 1 + } + + return 0 + }) +} + +// setLuaPreviewerPriority updates priority value for previewer with given name. +// When `withSort` is true, this function will sort previewer list when previewer +// priority is actually changed. +// If no previewer with given name is found, this function does nothing. +// This function returns true if previewer priority is actually changed, otherwise +// false. +func setLuaPreviewerPriority(name string, priority int, withSort bool) bool { + changed := false + + for i := range gLuaRegistry.previewers { + if gLuaRegistry.previewers[i].name == name { + if gLuaRegistry.previewers[i].priority != priority { + changed = true + gLuaRegistry.previewers[i].priority = priority + } + break + } + } + + if changed && withSort { + sortLuaPreviewers() + } + + return changed +} + +// loadSortingMethodRegistryFromTbl registers sort methods defined in table returned +// from plugin script. +func loadSortingMethodRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeySortingMethod + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + if gLuaRegistry.sortingMethod == nil { + gLuaRegistry.sortingMethod = make(map[string]*luaMsgExpr) + } + + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("sort method registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + msg := key.String() + name := getPluginNameForSourcePath(sourceName) + "." + msg + + switch value.Type() { + case lua.LTFunction: + gLuaRegistry.sortingMethod[name] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: false, + } + case lua.LTTable: + gLuaRegistry.sortingMethod[name] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: msg, + variant: luaMsgVariantMain, + isAsync: lua.LVAsBool(value.(*lua.LTable).RawGetString(luaMsgMetaKeyIsAsync)), + } + default: + log.Printf("unsupported sort method registry value for key: %s", msg) + return + } + + log.Printf("add sort method: %s", name) + }) +} + +// loadUIFormatterRegistryFromTbl registers UI formatters defined in table returned +// from plugin script. +func loadUIFormatterRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeyUIFormatter + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + if gLuaRegistry.uiFormatter == nil { + gLuaRegistry.uiFormatter = make(map[string]*luaMsgExpr) + } + + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("UI formatter key is expected to be string, found %s: %s", key.Type(), key) + return + } + + option := key.String() + switch option { + case luaUIFormatterCursorActive, + luaUIFormatterCursorParent, + luaUIFormatterCursorPreview, + luaUIFormatterError, + luaUIFormatterNumberCursor, + luaUIFormatterNumber, + luaUIFormatterTag: + // ok + default: + log.Println("unsupported UI formatter registry key:", option) + return + } + + switch value.Type() { + case lua.LTFunction: + gLuaRegistry.uiFormatter[option] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: option, + variant: luaMsgVariantMain, + } + case lua.LTTable: + actionValue := value.(*lua.LTable).RawGetString(luaCommandActionFuncKey) + if actionValue.Type() == lua.LTFunction { + gLuaRegistry.uiFormatter[option] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: option, + variant: luaMsgVariantMain, + isAsync: lua.LVAsBool(value.(*lua.LTable).RawGetString(luaMsgMetaKeyIsAsync)), + } + } else { + log.Println("invalid UI formatter value:", value) + } + default: + log.Printf("invalid UI formatter registry value of type %s: %s", value.Type(), value) + } + }) +} + +// loadUIPrinterRegistryFromTbl registers UI printer defined in table returned +// from plugin script. +func loadUIPrinterRegistryFromTbl(sourceName string, tbl *lua.LTable) { + registryKey := registryKeyUIPrinter + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + if gLuaRegistry.uiPrinter == nil { + gLuaRegistry.uiPrinter = make(map[string]*luaMsgExpr) + } + + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("UI pinter key is expected to be string, found %s: %s", key.Type(), key) + return + } + + option := key.String() + switch option { + case luaUIPrinterDirEntry, + luaUIPrinterDirectory, + luaUIPrinterRuler, + luaUIPrinterPrompt: + // ok + default: + log.Println("unsupported UI pinter registry key:", option) + return + } + + switch value.Type() { + case lua.LTFunction: + gLuaRegistry.uiPrinter[option] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: option, + variant: luaMsgVariantMain, + } + case lua.LTTable: + actionValue := value.(*lua.LTable).RawGetString(luaCommandActionFuncKey) + if actionValue.Type() == lua.LTFunction { + gLuaRegistry.uiPrinter[option] = &luaMsgExpr{ + sourceName: sourceName, + registry: registryKey, + msg: option, + variant: luaMsgVariantMain, + isAsync: lua.LVAsBool(value.(*lua.LTable).RawGetString(luaMsgMetaKeyIsAsync)), + } + } else { + log.Println("invalid UI pinter value:", value) + } + default: + log.Printf("invalid UI printer registry value of type %s: %s", value.Type(), value) + } + }) +} + +// loadUIStyleRegistryFromTbl loads UI styles defined in table into Go map. +func loadUIStyleRegistryFromTbl(L *lua.LState, sourceName string, tbl *lua.LTable) { + registryKey := registryKeyUIStyle + registryTbl := tbl.RawGetString(registryKey) + switch registryTbl.Type() { + case lua.LTNil: + return + case lua.LTTable: + // ok + default: + log.Printf("registry field `%s` is not a table", registryKey) + return + } + + if gLuaRegistry.uiStyleMap == nil { + gLuaRegistry.uiStyleMap = make(map[string]tcell.Style) + } + + registryTbl.(*lua.LTable).ForEach(func(key, value lua.LValue) { + if key.Type() != lua.LTString { + log.Printf("ui style registry key is expected to be string, found %s: %s", key.Type(), key) + return + } + + settingName := key.String() + option := value + + switch settingName { + case luaUIStyleBorder, + luaUIStyleCopy, + luaUIStyleCut, + luaUIStyleMenu, + luaUIStyleMenuheader, + luaUIStyleMenuselect, + luaUIStyleSelect, + luaUIStyleVisual: + // ok + default: + log.Println("unsupported UI style registry key:", settingName) + return + } + + if option.Type() == lua.LTFunction { + if err := L.CallByParam(lua.P{ + Fn: value.(*lua.LFunction), + NRet: 1, + Protect: true, + }); err == nil { + option = L.Get(-1) + L.Pop(1) + } else { + log.Printf("failed to evaluate UI style registry function for key `%s`: %s", settingName, err) + return + } + } + + if option.Type() == lua.LTUserData { + ud := value.(*lua.LUserData) + if style, ok := ud.Value.(*tcell.Style); ok { + gLuaRegistry.uiStyleMap[settingName] = *style + } else { + log.Printf("invalid UI style registry user data for key: %s", settingName) + } + } else { + log.Printf("unsupported UI style registry value type for key: %s", settingName) + } + }) +} + +// ---------------------------------------------------------------------------- +// message call operation + +// luaMsgActionExtractor takes Lua state and a message entry, and should return +// a Lua function pointer as action function of this message entry. When no valid +// action can be made for given message entry, this function returns nil. +type luaMsgActionExtractor func(L *lua.LState, msgEntry lua.LValue) *lua.LFunction + +// luaMsgArgsMaker is message argument type converter function, it takes Lua state +// and returns a slice of Lua values as arguments for message action. This will +// be used for Go value conversion after determining actual Lua State used for +// running message call. +type luaMsgArgsMaker func(L *lua.LState) []lua.LValue + +type luaMsgCallArgs struct { + sourceName, registryKey, msg, variant string + isAsync bool // if this message should be called on asynchronous Lua state + getArgs luaMsgArgsMaker +} + +var gLuaMsgActionExtractorMap = map[string]map[string]luaMsgActionExtractor{ + registryKeyCommand: { + luaMsgVariantMain: extractLuaMsgActionWithTblKey(luaCommandActionFuncKey), + luaCommandCompletionFuncKey: extractLuaMsgActionWithDefaultAction( + luaCommandCompletionFuncKey, + func(L *lua.LState) int { + return 0 + }, + ), + }, + registryKeyEventHook: { + luaMsgVariantMain: extractLuaMsgActionWithTblKey(luaEventHookActionFuncKey), + }, + registryKeyMisc: { + luaMsgVariantMain: extractLuaMsgActionWithTblKey(luaMiscActionFuncKey), + }, + registryKeyPreviewer: { + luaMsgVariantMain: extractLuaMsgActionWithTblKey(luaPreviewerActionFuncKey), + luaPreviewerCleanFuncKey: extractLuaMsgActionWithTblKey(luaPreviewerCleanFuncKey), + luaPreviewerConditionFuncKey: extractLuaMsgActionWithDefaultAction( + luaPreviewerConditionFuncKey, + func(L *lua.LState) int { + L.Push(lua.LTrue) + return 1 + }, + ), + }, + registryKeySortingMethod: { + luaMsgVariantMain: extractLuaMsgActionWithTblKey(luaSortingMethodActionFuncKey), + }, + registryKeyUIFormatter: { + luaMsgVariantMain: extractLuaMsgActionWithTblKey(luaUIFormatterActionFuncKey), + }, + registryKeyUIPrinter: { + luaMsgVariantMain: extractLuaMsgActionWithTblKey(luaUIPrinterActionFuncKey), + }, +} + +// extractLuaMsgActionWithTblKey returns a action extractor function, returned +// function will retruns message entry as a Lua function if it is one, if that +// entry is a table, extractor will check the value for given key, and returns +// that value if it is a function. +// Otherwise extractor returns nil. +func extractLuaMsgActionWithTblKey(key string) luaMsgActionExtractor { + return func(L *lua.LState, msgEntry lua.LValue) *lua.LFunction { + switch msgEntry.Type() { + case lua.LTFunction: + // if entry is a function, return it as is. + return msgEntry.(*lua.LFunction) + case lua.LTTable: + // if entry is a table, try to get actioin function with given key + actionFunc := msgEntry.(*lua.LTable).RawGetString(key) + if actionFunc.Type() == lua.LTFunction { + return actionFunc.(*lua.LFunction) + } + } + // invalid entry + return nil + } +} + +// extractLuaMsgActionWithDefaultAction returns a action extractor that works +// pretty much like extractLuaMsgActionWithTblKey ones, but will return a Lua +// function made by wrapping `defualtAction` when message entry is defined as +// a function itself, or is defined as a table but does not contains specified key. +func extractLuaMsgActionWithDefaultAction(key string, defaultAction lua.LGFunction) luaMsgActionExtractor { + return func(L *lua.LState, msgEntry lua.LValue) *lua.LFunction { + switch msgEntry.Type() { + case lua.LTFunction: + return L.NewFunction(defaultAction) + case lua.LTTable: + actionFunc := msgEntry.(*lua.LTable).RawGetString(key) + switch actionFunc.Type() { + case lua.LTFunction: + return actionFunc.(*lua.LFunction) + case lua.LTNil: + return L.NewFunction(defaultAction) + } + } + // invalid entry + return nil + } +} + +// getLuaMsgEntry looks up Lua registry table for target message entry. +func getLuaMsgEntry(L *lua.LState, sourceName string, registryKey string, msg string) (lua.LValue, error) { + registryMap := gLuaRegistry.stateDataMap[L] + if registryMap == nil { + return nil, fmt.Errorf("no registry data found for current Lua state") + } + + tbl := registryMap[sourceName] + if tbl == nil { + return nil, fmt.Errorf("invalid msg source name: %s", sourceName) + } + + value := tbl.RawGetString(registryKey) + switch value.Type() { + case lua.LTNil: + return nil, fmt.Errorf("no handler table found for registry key `%s`", registryKey) + case lua.LTTable: + // ok + default: + return nil, fmt.Errorf("unexpected type of registry value for key `%s`: %s", registryKey, value.Type()) + } + + handlerTbl := value.(*lua.LTable) + handler := handlerTbl.RawGetString(msg) + + return handler, nil +} + +// getLuaMsgAction finds message action of specified message. +func getLuaMsgAction(L *lua.LState, sourceName, registryKey, msg, variant string) (*lua.LFunction, error) { + entry, err := getLuaMsgEntry(L, sourceName, registryKey, msg) + if err != nil { + return nil, fmt.Errorf("failed to get msg entry: %s", err) + } + + var action *lua.LFunction + var extractor luaMsgActionExtractor + if extractorMap, ok := gLuaMsgActionExtractorMap[registryKey]; ok { + extractor = extractorMap[variant] + } + + if extractor != nil { + action = extractor(L, entry) + } else { + action, _ = entry.(*lua.LFunction) + } + if action == nil { + return nil, fmt.Errorf("failed to get valid msg action function") + } + + return action, nil +} + +// makeLuaMsgArgsWrapper returns a luaMsgArgsMaker function that converts all +// of passed arguments into Lua value slices. +func makeLuaMsgArgsWrapper(args ...any) luaMsgArgsMaker { + return func(L *lua.LState) []lua.LValue { + result := make([]lua.LValue, len(args)) + for i, arg := range args { + if value, err := goValueToLuaValue(L, arg); err == nil { + result[i] = value + } else { + log.Printf("lua message argument wrapper error at value %v: %s", arg, err) + result[i] = lua.LNil + } + } + return result + } +} + +// callLuaMsgOnState finds and runs target Lua message action on given Lua state. +func callLuaMsgOnState(L *lua.LState, callArgs luaMsgCallArgs) ([]lua.LValue, error) { + if !gOpts.luamsglog { + // pass + } else if callArgs.variant != "" { + log.Printf("call Lua msg: (%s, %s, %s)@%s", callArgs.sourceName, callArgs.registryKey, callArgs.msg, callArgs.variant) + } else { + log.Printf("call Lua msg: (%s, %s, %s)", callArgs.sourceName, callArgs.registryKey, callArgs.msg) + } + + action, err := getLuaMsgAction(L, callArgs.sourceName, callArgs.registryKey, callArgs.msg, callArgs.variant) + if err != nil { + return nil, err + } + + var luaArgs []lua.LValue + if callArgs.getArgs != nil { + luaArgs = callArgs.getArgs(L) + } + + oldTop := L.GetTop() + + L.Push(action) + for _, arg := range luaArgs { + L.Push(arg) + } + + err = L.PCall(len(luaArgs), lua.MultRet, nil) + if err != nil { + return nil, err + } + + nRet := L.GetTop() - oldTop + defer L.Pop(nRet) + if nRet <= 0 { + return nil, nil + } + + ret := make([]lua.LValue, nRet) + for i := 0; i < nRet; i++ { + ret[i] = L.Get(oldTop + i + 1) + } + + return ret, nil +} + +// callLuaMsgAsync gets a Lua state from pool and runs target Lua message on it. +func callLuaMsgAsync(callArgs luaMsgCallArgs) ([]lua.LValue, error) { + L, err := gLuaPool.get() + if err != nil { + return nil, err + } + defer gLuaPool.put(L) + return callLuaMsgOnState(L, callArgs) +} + +// callLuaMsgSync acquires global synchronous Lua state and runs target Lua message +// on it. +func callLuaMsgSync(callArgs luaMsgCallArgs) ([]lua.LValue, error) { + L, err := gLuaPool.acquireSyncState() + defer gLuaPool.releaseSyncState() + + if err != nil { + return nil, err + } + + return callLuaMsgOnState(L, callArgs) +} + +// callLuaMsg calls Lua message specified in call argument, this function will use +// the `isAsync` flag in argument to determine which Lua state source to use. +func callLuaMsg(callArgs luaMsgCallArgs) ([]lua.LValue, error) { + if callArgs.isAsync { + return callLuaMsgAsync(callArgs) + } else { + return callLuaMsgSync(callArgs) + } +} + +// callLuaMsgExpr calls Lua message specified by Lua message expression. +func callLuaMsgExpr(expr *luaMsgExpr, getArgs luaMsgArgsMaker) ([]lua.LValue, error) { + return callLuaMsg(luaMsgCallArgs{ + sourceName: expr.sourceName, + registryKey: expr.registry, + msg: expr.msg, + variant: expr.variant, + isAsync: expr.isAsync, + getArgs: getArgs, + }) +} + +// ---------------------------------------------------------------------------- +// command + +// callLuaCommandCompletion calls completion message for given Lua command. +func callLuaCommandCompletion(expr *luaMsgExpr, args []string, longest string) ([]compMatch, string) { + ret, err := callLuaMsg( + luaMsgCallArgs{ + sourceName: expr.sourceName, + registryKey: expr.registry, + msg: expr.msg, + variant: luaCommandCompletionFuncKey, + isAsync: expr.isAsync, + getArgs: func(L *lua.LState) []lua.LValue { + tbl := L.NewTable() + for i, arg := range args { + tbl.RawSetInt(i+1, lua.LString(arg)) + } + return []lua.LValue{tbl, lua.LString(longest)} + }, + }, + ) + + var matches []compMatch + + if err != nil { + log.Printf("failed to call Lua command completion function: %s", err) + return matches, longest + } + + nRet := len(ret) + if nRet == 0 { + return matches, longest + } + + ret1 := ret[0] + switch ret1.Type() { + case lua.LTTable: + matchesTbl := ret1.(*lua.LTable) + + cnt := matchesTbl.Len() + for i := 1; i <= cnt; i++ { + value := matchesTbl.RawGetInt(i) + switch value.Type() { + case lua.LTString: + str := value.String() + matches = append(matches, compMatch{str, str}) + case lua.LTUserData: + if v, ok := value.(*lua.LUserData).Value.(*compMatch); ok { + matches = append(matches, *v) + } else { + log.Printf("matches list element #%d is not a valid user data", i) + } + default: + log.Printf("matches list element #%d be string or user data, found %s: %s", i, value.Type(), value) + } + } + case lua.LTNil: + // pass + default: + log.Println("return value #1 of completion function should be a table") + return matches, longest + } + + if len(ret) >= 2 { + ret2 := ret[1] + switch ret2.Type() { + case lua.LTString: + longest = ret2.String() + case lua.LTNil: + // pass + default: + log.Println("return value #2 of completion function should be a string") + return nil, longest + } + } + + return matches, longest +} + +// ---------------------------------------------------------------------------- +// event hook + +// callLuaEventHooks calls all Lua event hooks under given command name. +func callLuaEventHooks(cmdName string, getArgs luaMsgArgsMaker) { + exprList, ok := gLuaRegistry.eventHooks[cmdName] + if !ok { + return + } + + errCnt := 0 + for _, expr := range exprList { + _, err := callLuaMsgExpr(expr, getArgs) + if err != nil { + errCnt++ + log.Printf("failed to run hook %s: %s", expr, err) + } + } + + if errCnt > 0 { + log.Printf("%d error(s) occured during event hook call, see log for more detail", errCnt) + } +} + +// ---------------------------------------------------------------------------- +// previewer + +type luaPreviewerPipe struct { + m sync.Mutex + buf *bytes.Buffer + + ticker *time.Ticker + wake chan struct{} + + readSideClosed bool + closed bool + done chan struct{} + + volatile bool + + previewErr error +} + +func newLuaPreviewerPipe() *luaPreviewerPipe { + lp := &luaPreviewerPipe{ + buf: new(bytes.Buffer), + + ticker: time.NewTicker(10 * time.Millisecond), + wake: make(chan struct{}, 1), + done: make(chan struct{}), + } + + go lp.wakeupLoop() + + return lp +} + +// wakeupLoop runs in background goroutine, wakes blocked `Read` call from time +// to time for checking new content in pipe buffer. +func (lp *luaPreviewerPipe) wakeupLoop() { + for { + select { + case <-lp.ticker.C: + select { + case lp.wake <- struct{}{}: + default: + } + case <-lp.done: + return + } + } +} + +// Write puts data to pipe buffer. Returns `io.ErrClosedPipe` when pipe is closed, +// so that writing side can known no more data is required on reading side. +func (lp *luaPreviewerPipe) Write(p []byte) (n int, err error) { + lp.m.Lock() + defer lp.m.Unlock() + + if lp.readSideClosed || lp.closed { + return 0, io.ErrClosedPipe + } + + return lp.buf.Write(p) +} + +// Read tries to read from pipe buffer, and if there is currently nothing to read, +// this function will wait for small amount of time and then try reading again. +func (lp *luaPreviewerPipe) Read(p []byte) (n int, err error) { + maxTry := 100 + + for range maxTry { + lp.m.Lock() + + n, _ = lp.buf.Read(p) + if n > 0 { + lp.m.Unlock() + return n, nil + } + + if lp.closed { + lp.m.Unlock() + return 0, io.EOF + } + + lp.m.Unlock() + + select { + case <-lp.wake: + continue + case <-lp.done: + continue + case <-time.After(10 * time.Second): + return 0, errors.New("previewer pipe read timeout") + } + } + + return 0, fmt.Errorf("previewer reads nothing after %d attempt", maxTry) +} + +func (lp *luaPreviewerPipe) Close() error { + lp.m.Lock() + defer lp.m.Unlock() + + if lp.closed { + return nil + } + + lp.closed = true + + close(lp.done) + lp.ticker.Stop() + + return nil +} + +func (lp *luaPreviewerPipe) closeReadSide() { + lp.m.Lock() + defer lp.m.Unlock() + + lp.readSideClosed = true +} + +// setVolatile updates volatile indicator on pipe. +func (lp *luaPreviewerPipe) setVolatile(isVolatile bool) { + lp.m.Lock() + defer lp.m.Unlock() + + lp.volatile = isVolatile +} + +// isVolatile returns flag indicating if preview result should be marked as volatile. +func (lp *luaPreviewerPipe) isVolatile() bool { + lp.m.Lock() + defer lp.m.Unlock() + + return lp.volatile +} + +// wait blocks goroutine until previewer pip is closed. +func (lp *luaPreviewerPipe) wait() { + <-lp.done +} + +// setPreviewError bind Lua execution error to pipe. +func (lp *luaPreviewerPipe) setPreviewError(err error) { + lp.m.Lock() + defer lp.m.Unlock() + + lp.previewErr = err +} + +// checkPreviewError retruns Lua execution error binded to pipe. +func (lp *luaPreviewerPipe) checkPreviewError() error { + lp.m.Lock() + defer lp.m.Unlock() + return lp.previewErr +} + +// callLuaPreviewerConditionChecker calls condition message for given Lua previewer. +// And returns a bool flag indicating if this previewer is active for given argument. +func callLuaPreviewerConditionChecker(expr *luaMsgExpr, path string) (bool, error) { + ret, err := callLuaMsg(luaMsgCallArgs{ + sourceName: expr.sourceName, + registryKey: expr.registry, + msg: expr.msg, + variant: luaPreviewerConditionFuncKey, + isAsync: expr.isAsync, + getArgs: func(L *lua.LState) []lua.LValue { + return []lua.LValue{lua.LString(path)} + }, + }) + + if err != nil { + return false, err + } + + if len(ret) == 0 { + return false, nil + } + + return lua.LVAsBool(ret[0]), nil +} + +// callLuaPreviewerAction calls action message for given Lua previewer. when this +// function returns true, preview content should be marked as volatile, just link +// an non-zero exit code returned by previewer command. +func callLuaPreviewerAction(expr *luaMsgExpr, path string, w, h, x, y int, mode string) *luaPreviewerPipe { + pipe := newLuaPreviewerPipe() + + go func() { + writer := bufio.NewWriter(pipe) + defer func() { + var err error + err = writer.Flush() + if err != nil { + log.Printf("failed to flush Lua preview writer: %s", err) + } + + err = pipe.Close() + if err != nil { + log.Printf("failed to close Lua preview pipe: %s", err) + } + }() + + ret, err := callLuaMsgExpr(expr, func(L *lua.LState) []lua.LValue { + return []lua.LValue{ + lWrapBufWriter(L, writer), + lua.LString(path), + lua.LNumber(w), + lua.LNumber(h), + lua.LNumber(x), + lua.LNumber(y), + lua.LString(mode), + } + }) + + if err != nil { + pipe.setPreviewError(err) + pipe.setVolatile(false) + return + } + + nRet := len(ret) + if nRet > 0 { + pipe.setVolatile(lua.LVAsBool(ret[0])) + } + + if nRet > 1 { + luaErr := ret[1] + if luaErr.Type() != lua.LTNil { + pipe.setPreviewError(errors.New(luaErr.String())) + } + } + }() + + return pipe +} + +// callLuaPreviewerCleaning calls clean message for given Lua previewer. +func callLuaPreviewerCleaning(expr *luaMsgExpr, previousFile string, w, h, x, y int, nextFile string) error { + _, err := callLuaMsg(luaMsgCallArgs{ + sourceName: expr.sourceName, + registryKey: expr.registry, + msg: expr.msg, + variant: luaPreviewerCleanFuncKey, + isAsync: expr.isAsync, + getArgs: func(L *lua.LState) []lua.LValue { + return []lua.LValue{ + lua.LString(previousFile), + lua.LNumber(w), + lua.LNumber(h), + lua.LNumber(x), + lua.LNumber(y), + lua.LString(nextFile), + } + }, + }) + + return err +} + +// getLuaPreviewerForPath search for active previewer for certain path. +func getLuaPreviewerForPath(path string) *luaPreviewerInfo { + var result *luaPreviewerInfo + for i := range gLuaRegistry.previewers { + previewer := &gLuaRegistry.previewers[i] + ok, err := callLuaPreviewerConditionChecker(&previewer.msgexpr, path) + if err != nil { + log.Printf("failed to check condition for previewer %s: %s", previewer.name, err) + } else if ok { + result = previewer + break + } + } + + return result +} + +// getLuaPreviewerForPathOnState checks avtivation for each Lua previewer on +// give Lua state. +func getLuaPreviewerForPathOnState(L *lua.LState, path string) *luaPreviewerInfo { + var result *luaPreviewerInfo + for i := range gLuaRegistry.previewers { + previewer := &gLuaRegistry.previewers[i] + + expr := previewer.msgexpr + ret, err := callLuaMsgOnState(L, luaMsgCallArgs{ + sourceName: expr.sourceName, + registryKey: expr.registry, + msg: expr.msg, + variant: luaPreviewerConditionFuncKey, + isAsync: expr.isAsync, + getArgs: func(L *lua.LState) []lua.LValue { + return []lua.LValue{lua.LString(path)} + }, + }) + + if err != nil { + log.Printf("failed to check condition for previewer %s: %s", previewer.name, err) + continue + } + + if len(ret) == 0 { + continue + } + + log.Println(previewer, ret) + + if lua.LVAsBool(ret[0]) { + result = previewer + break + } + } + + return result +} + +// getLuaPreviewerNames returns name list of all registered Lua previewers. +func getLuaPreviewerNames() []string { + names := make([]string, len(gLuaRegistry.previewers)) + for i, previewer := range gLuaRegistry.previewers { + names[i] = previewer.name + } + return names +} + +// callLuaKeyMapMsgOnState calls Lua key map message on given Lua state. +func callLuaKeyMapMsgOnState(L *lua.LState, expr *luaKeyMapExpr) error { + if gOpts.luamsglog { + log.Printf("call Lua key map: %s - %s.%s", expr.sourceName, expr.keyMapType, expr.key) + } + + keyMapGroup, err := getLuaMsgEntry(L, expr.sourceName, registryKeyKeyMap, expr.keyMapType) + if err != nil { + return err + } + + groupTbl, ok := keyMapGroup.(*lua.LTable) + if !ok { + return fmt.Errorf("key map group is not table value") + } + + var action *lua.LFunction + + entry := groupTbl.RawGetString(expr.key) + switch entry.Type() { + case lua.LTFunction: + action = entry.(*lua.LFunction) + case lua.LTTable: + value := entry.(*lua.LTable).RawGetString(luaKeyMapActionFuncKey) + action, _ = value.(*lua.LFunction) + case lua.LTNil: + return fmt.Errorf("no action found") + default: + return fmt.Errorf("not supported action value type") + } + + if action == nil { + return fmt.Errorf("no action found") + } + + L.Push(action) + L.Push(lua.LNumber(expr.count)) + + err = L.PCall(1, 0, nil) + if err != nil { + return err + } + + return nil +} + +// callLuaKeyMapMsg calls Lua key map message, pick proper Lua state source according +// to `isAsync` flag in message expression. +func callLuaKeyMapMsg(expr *luaKeyMapExpr) error { + if expr.isAsync { + L, err := gLuaPool.get() + if err != nil { + return err + } + defer gLuaPool.put(L) + return callLuaKeyMapMsgOnState(L, expr) + } else { + L, err := gLuaPool.acquireSyncState() + defer gLuaPool.releaseSyncState() + + if err != nil { + return err + } + + return callLuaKeyMapMsgOnState(L, expr) + } +} + +// ---------------------------------------------------------------------------- +// sorting method + +// getLuaSortingMethodNames returns name list of all registered Lua sort method. +func getLuaSortingMethodNames() []string { + return slices.Collect(maps.Keys(gLuaRegistry.sortingMethod)) +} + +// getLuaSortingMethod returns Lua message expression for sort method with given name. +func getLuaSortingMethod(name string) *luaMsgExpr { + return gLuaRegistry.sortingMethod[name] +} + +// sortByLuaMsgOnState calls Lua sorting method message on given Lua state. +func sortByLuaMsgOnState(L *lua.LState, expr *luaMsgExpr, dir *dir) error { + action, err := getLuaMsgAction(L, expr.sourceName, expr.registry, expr.msg, expr.variant) + if err != nil { + return err + } + + options := L.NewTable() + options.RawSetString("dircounts", lua.LBool(dir.dircounts)) + options.RawSetString("dirfirst", lua.LBool(dir.dirfirst)) + options.RawSetString("dironly", lua.LBool(dir.dironly)) + options.RawSetString("hidden", lua.LBool(dir.hidden)) + options.RawSetString("sortignorecase", lua.LBool(dir.sortignorecase)) + options.RawSetString("sortignoredia", lua.LBool(dir.sortignoredia)) + + udCache := make(map[*file]*lua.LUserData) + + slices.SortStableFunc(dir.files, func(f1, f2 *file) int { + ud1 := udCache[f1] + if ud1 == nil { + ud1 = lWrapFile(L, f1) + udCache[f1] = ud1 + } + + ud2 := udCache[f2] + if ud2 == nil { + ud2 = lWrapFile(L, f2) + udCache[f2] = ud2 + } + + L.Push(action) + if dir.reverse { + L.Push(ud2) + L.Push(ud1) + } else { + L.Push(ud1) + L.Push(ud2) + } + + L.Push(options) + + err = L.PCall(3, 1, nil) + if err != nil { + return 0 + } + + ret := L.Get(-1) + defer L.Pop(1) + + num, ok := ret.(lua.LNumber) + if !ok { + err = fmt.Errorf("lua sortting message returns non-numeric value") + } + + return int(num) + }) + + return err +} + +// sortByLuaMsg pass given file list to Lua sort method and update file list order +// in place. +func sortByLuaMsg(expr *luaMsgExpr, dir *dir) error { + if expr.isAsync { + L, err := gLuaPool.get() + if err != nil { + return err + } + defer gLuaPool.put(L) + return sortByLuaMsgOnState(L, expr, dir) + } else { + L, err := gLuaPool.acquireSyncState() + defer gLuaPool.releaseSyncState() + + if err != nil { + return err + } + + return sortByLuaMsgOnState(L, expr, dir) + } +} + +// ---------------------------------------------------------------------------- +// UI Formatter + +// getLuaUIFormatter finds Lua UI formatter message with given name. +func getLuaUIFormatter(name string) *luaMsgExpr { + return gLuaRegistry.uiFormatter[name] +} + +// callLuaUIFormatter calls a Lua UI formatter message and returns the string +// build by formatter. +func callLuaUIFormatter(expr *luaMsgExpr, getArgs luaMsgArgsMaker) (string, error) { + ret, err := callLuaMsgExpr(expr, getArgs) + if err != nil { + return "", err + } + + if len(ret) == 0 { + return "", fmt.Errorf("lua UI formatter does not return a string") + } + + value := ret[0] + if value.Type() != lua.LTString { + return "", fmt.Errorf("lua UI formatter does not return a string") + } + + return string(value.(lua.LString)), nil +} + +// callLuaUIFormatterIgnoreError is a wrapper of callLuaUIFormatter which logs +// any error returned by callLuaUIFormatter without returning it. +func callLuaUIFormatterIgnoreError(expr *luaMsgExpr, getArgs luaMsgArgsMaker) string { + ret, err := callLuaUIFormatter(expr, getArgs) + if err != nil { + log.Printf("failed to execute Lua UI formatter %s: %s", expr, err) + } + return ret +} + +// callLuaUIFormatterWithSingleParam calls Lua UI formatter with single string +// parameter, if such formatter does not exists, a string build with given default +// format string will be returned. +func callLuaUIFormatterWithSingleParam(formatterName, defaultFmtStr, param string) string { + luaFormatter := getLuaUIFormatter(formatterName) + if luaFormatter != nil { + return callLuaUIFormatterIgnoreError(luaFormatter, makeLuaMsgArgsWrapper(param)) + } + return fmt.Sprintf(optionToFmtstr(defaultFmtStr), param) +} + +// ---------------------------------------------------------------------------- +// UI printer + +// getLuaUIPrinter finds Lua UI printer message with given name. +func getLuaUIPrinter(name string) *luaMsgExpr { + return gLuaRegistry.uiPrinter[name] +} + +// ---------------------------------------------------------------------------- +// UI Style + +// getLuaUIStyleWithDefaultStr looks up Lua UI style registry with given name. +// When target key does not exists, make a new style object with default format +// string. +func getLuaUIStyleWithDefaultStr(name string, defaultFmtStr string) tcell.Style { + style, ok := gLuaRegistry.uiStyleMap[name] + if !ok { + style = parseEscapeSequence(defaultFmtStr) + } + return style +} + +// ---------------------------------------------------------------------------- +// MISC + +// getLuaMiscMsg returns misc message with given name. If no such message is registered, +// this function returns nil. +func getLuaMiscMsg(name string) *luaMsgExpr { + return gLuaRegistry.misc[name] +} + +func formatDuplicatedFilenameWithLuaMsg(expr *luaMsgExpr, basename, ext string, dupIndex int) (string, error) { + ret, err := callLuaMsgExpr(expr, makeLuaMsgArgsWrapper(basename, ext, dupIndex)) + if err != nil { + return "", err + } + + if len(ret) <= 0 { + return "", fmt.Errorf("lua message returns nonthing") + } + + strValue, ok := ret[0].(lua.LString) + if !ok { + return "", fmt.Errorf("return value #1 is not a string") + } + + return string(strValue), nil +} + +// makeShellCmdWithLuaMsg creates `exec.Cmd` object by calling Lua message. +func makeShellCmdWithLuaMsg(expr *luaMsgExpr, cmd_name string, args []string) (*exec.Cmd, error) { + ret, err := callLuaMsgExpr(expr, makeLuaMsgArgsWrapper(cmd_name, args)) + if err != nil { + return nil, err + } + + if len(ret) <= 0 { + return nil, fmt.Errorf("lua shell command maker returns nonthing") + } + + ret1 := ret[0] + ud, ok := ret1.(*lua.LUserData) + if !ok { + return nil, fmt.Errorf("return value #1 of Lua shell command maker is not a userdata") + } + + cmd, ok := ud.Value.(*exec.Cmd) + if !ok { + return nil, fmt.Errorf("return value #1 of Lua shell command is not a Cmd object") + } + + return cmd, nil +} diff --git a/main.go b/main.go index 7d988794..ada1de04 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ var ( gLogPath string gSelect string gConfigPath string + gPluginDir string gCommands arrayFlag gVersion string ) @@ -92,6 +93,7 @@ func exportEnvVars() { func exportFlags() { os.Setenv("lf_flag_config", gConfigPath) + os.Setenv("lf_flag_plugin_dir", gPluginDir) os.Setenv("lf_flag_last_dir_path", gLastDirPath) os.Setenv("lf_flag_log", gLogPath) os.Setenv("lf_flag_print_last_dir", strconv.FormatBool(gPrintLastDir)) @@ -300,6 +302,11 @@ Options: "", "`path` to the config file (instead of the usual paths)") + flag.StringVar(&gPluginDir, + "plugin-dir", + "", + "`path` to plugin directory (overriding default paths)") + flag.Var(&gCommands, "command", "`command` to execute on client initialization") diff --git a/misc.go b/misc.go index 2fe15310..21242965 100644 --- a/misc.go +++ b/misc.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/fs" + "log" "math/big" "os" "path/filepath" @@ -592,6 +593,32 @@ func getWidths(wtot int, ratios []int, drawbox bool, borderstyle borderStyle) [] return widths } +// formatDuplicatedFilename returns name used for duplicated files during copy +// or move operation. +func formatDuplicatedFilename(basename, ext string, dupIndex int) string { + msgExpr := getLuaMiscMsg(luaMiscMsgDupFile) + if msgExpr != nil { + file, err := formatDuplicatedFilenameWithLuaMsg(msgExpr, basename, ext, dupIndex) + if file == "" { + log.Printf("Lua duplicated file name formatter error: %s", err) + } else { + return file + } + } + + file := strings.ReplaceAll(gOpts.dupfilefmt, "%f", basename+ext) + file = strings.ReplaceAll(file, "%b", basename) + file = strings.ReplaceAll(file, "%e", ext) + file = strings.ReplaceAll(file, "%n", strconv.Itoa(dupIndex)) + + return file +} + +// formatDisplayedErrorMsg returns displayed format of error message +func formatDisplayedErrorMsg(msg string) string { + return callLuaUIFormatterWithSingleParam(luaUIFormatterError, gOpts.errorfmt, sanitizeName(msg)) +} + // We don't need no generic code // We don't need no type control // No dark templates in compiler diff --git a/nav.go b/nav.go index 397aab35..4ba9bcea 100644 --- a/nav.go +++ b/nav.go @@ -31,18 +31,19 @@ const ( ) type file struct { - os.FileInfo // stat information - linkState linkState // symlink state - linkTarget string // path a symlink points to - path string // full path including the name - dirCount int // number of items inside the directory - dirSize int64 // total directory size (needs to be calculated via `calcdirsize`) - accessTime time.Time // time of last access - birthTime time.Time // time of file birth - changeTime time.Time // time of last status (inode) change - customInfo string // property defined via `addcustominfo` - ext string // file extension (including the dot) - err error // potential error returned by [os.Lstat] + os.FileInfo // stat information + linkState linkState // symlink state + linkTarget string // path a symlink points to + path string // full path including the name + dirCount int // number of items inside the directory + dirSize int64 // total directory size (needs to be calculated via `calcdirsize`) + accessTime time.Time // time of last access + birthTime time.Time // time of file birth + changeTime time.Time // time of last status (inode) change + customInfo string // property defined via `addcustominfo` + ext string // file extension (including the dot) + err error // potential error returned by [os.Lstat] + extraLuaData map[string]any // stores data set and used by Lua scripts } func newFile(path string) *file { @@ -127,10 +128,22 @@ func newFile(path string) *file { } } -func (file *file) isPreviewable() bool { +// isPreviewablePlain reports if file is previewable without calling any Lua message. +// This method is seperated to avoid deadlock caused by calling isPreviewable from +// Lua. +func (file *file) isPreviewablePlain() bool { return !file.IsDir() || gOpts.dirpreviews } +// isPreviewableLua reports if file is previewable by any Lua previewer. +func (file *file) isPreviewableLua() bool { + return getLuaPreviewerForPath(file.path) != nil +} + +func (file *file) isPreviewable() bool { + return file.isPreviewablePlain() || file.isPreviewableLua() +} + type fakeStat struct { name string } @@ -162,26 +175,27 @@ func readdir(path string) ([]*file, error) { } type dir struct { - loading bool // whether directory is loading from disk - loadTime time.Time // last load time - ind int // 0-based index of current entry in dir.files - pos int // 0-based cursor row in directory window - path string // full path of directory - files []*file // displayed files in directory including or excluding hidden ones - allFiles []*file // all files in directory including hidden ones (same array as files) - sortby sortMethod // sortby value from last sort - dircounts bool // dircounts value from last sort - dirfirst bool // dirfirst value from last sort - dironly bool // dironly value from last sort - hidden bool // hidden value from last sort - reverse bool // reverse value from last sort - visualAnchor int // index where Visual mode was initiated - visualWrap int // wrap direction in Visual mode (0: none, +: bottom->top, -: top->bottom) - hiddenfiles []string // hiddenfiles value from last sort - filter []string // last filter for this directory - sortignorecase bool // sortignorecase value from last sort - sortignoredia bool // sortignoredia value from last sort - noPerm bool // whether lf has no permission to open the directory + loading bool // whether directory is loading from disk + loadTime time.Time // last load time + ind int // 0-based index of current entry in dir.files + pos int // 0-based cursor row in directory window + path string // full path of directory + files []*file // displayed files in directory including or excluding hidden ones + allFiles []*file // all files in directory including hidden ones (same array as files) + sortby sortMethod // sortby value from last sort + dircounts bool // dircounts value from last sort + dirfirst bool // dirfirst value from last sort + dironly bool // dironly value from last sort + hidden bool // hidden value from last sort + reverse bool // reverse value from last sort + visualAnchor int // index where Visual mode was initiated + visualWrap int // wrap direction in Visual mode (0: none, +: bottom->top, -: top->bottom) + hiddenfiles []string // hiddenfiles value from last sort + filter []string // last filter for this directory + sortignorecase bool // sortignorecase value from last sort + sortignoredia bool // sortignoredia value from last sort + noPerm bool // whether lf has no permission to open the directory + extraLuaData map[string]any // stores data set and used by Lua scripts } func newDir(path string) *dir { @@ -318,6 +332,13 @@ func (dir *dir) sort() { s2 := normalize(stripTermSequence(f2.customInfo)) return naturalCmp(s1, s2) }) + default: + if msgExpr := getLuaSortingMethod(string(dir.sortby)); msgExpr != nil { + err := sortByLuaMsg(msgExpr, dir) + if err != nil { + log.Println(err) + } + } } // when sorting by size while also showing dircounts, we always display files @@ -784,28 +805,38 @@ func (nav *nav) previewLoop(ui *ui) { } } win := ui.wins[len(ui.wins)-1] - if isClear && len(gOpts.previewer) != 0 && len(gOpts.cleaner) != 0 && nav.volatilePreview { - cmd := exec.Command( - gOpts.cleaner, - prev, - strconv.Itoa(win.w), - strconv.Itoa(win.h), - strconv.Itoa(win.x), - strconv.Itoa(win.y), - path, - ) - var stderr bytes.Buffer - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - var exitErr *exec.ExitError - if !errors.As(err, &exitErr) { - log.Printf("cleaning preview: %s", err) + luaPreviewer := getLuaPreviewerForPath(prev) + luaCleanerOk := luaPreviewer != nil && luaPreviewer.hasCleaner + + if isClear && (len(gOpts.cleaner) != 0 || luaCleanerOk) && nav.volatilePreview { + if luaCleanerOk { + err := callLuaPreviewerCleaning(&luaPreviewer.msgexpr, prev, win.w, win.h, win.x, win.y, path) + if err != nil { + log.Printf("Lua cleaner error: %s", err) + } + } else if len(gOpts.previewer) != 0 { + cmd := exec.Command( + gOpts.cleaner, + prev, + strconv.Itoa(win.w), + strconv.Itoa(win.h), + strconv.Itoa(win.x), + strconv.Itoa(win.y), + path, + ) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) { + log.Printf("cleaning preview: %s", err) + } + } + if s := strings.TrimSpace(stderr.String()); s != "" { + s = strings.Join(strings.Fields(s), " ") + log.Printf("cleaning preview (stderr): %s", s) } - } - if s := strings.TrimSpace(stderr.String()); s != "" { - s = strings.Join(strings.Fields(s), " ") - log.Printf("cleaning preview (stderr): %s", s) } nav.volatilePreview = false } @@ -879,7 +910,22 @@ func (nav *nav) preview(path string, win *win, mode string) { var reader *bufio.Reader - if len(gOpts.previewer) != 0 { + luaPreviewer := getLuaPreviewerForPath(path) + + if luaPreviewer != nil { + pipe := callLuaPreviewerAction(&luaPreviewer.msgexpr, path, win.w, win.h, win.x, win.y, mode) + reader = bufio.NewReader(pipe) + + defer func() { + pipe.closeReadSide() + pipe.wait() + reg.volatile = pipe.isVolatile() + + if err := pipe.checkPreviewError(); err != nil { + log.Printf("Lua previewer error %s: %s", &luaPreviewer.msgexpr, err) + } + }() + } else if len(gOpts.previewer) != 0 { cmd := exec.Command( gOpts.previewer, path, @@ -950,7 +996,7 @@ func (nav *nav) preview(path string, win *win, mode string) { // escape sequences that corrupt the display or enable code execution // (e.g. OSC 52 clipboard writes). Replace control characters with // U+FFFD so they are visible but cannot form escape sequences. - if len(gOpts.previewer) == 0 && !binary { + if luaPreviewer == nil && len(gOpts.previewer) == 0 && !binary { sixel = false for i, l := range lines { lines[i] = sanitizePreview(l) @@ -1433,10 +1479,7 @@ func (nav *nav) moveAsync(app *app, srcs []string, dstDir string) { basename := file[:len(file)-len(ext)] var newPath string for i := 1; !os.IsNotExist(err); i++ { - file = strings.ReplaceAll(gOpts.dupfilefmt, "%f", basename+ext) - file = strings.ReplaceAll(file, "%b", basename) - file = strings.ReplaceAll(file, "%e", ext) - file = strings.ReplaceAll(file, "%n", strconv.Itoa(i)) + file = formatDuplicatedFilename(basename, ext, i) newPath = filepath.Join(dstDir, file) _, err = os.Lstat(newPath) } diff --git a/opts.go b/opts.go index afcee68f..43f5b791 100644 --- a/opts.go +++ b/opts.go @@ -26,7 +26,7 @@ func isValidSortMethod(method sortMethod) bool { case naturalSort, nameSort, sizeSort, timeSort, atimeSort, btimeSort, ctimeSort, extSort, customSort: return true } - return false + return getLuaSortingMethod(string(method)) != nil } const invalidSortErrorMessage = `sortby: value should either be 'natural', 'name', 'size', 'time', 'atime', 'btime', 'ctime', 'ext' or 'custom'` @@ -113,6 +113,7 @@ var gOpts struct { info []string infotimefmtnew string infotimefmtold string + luamsglog bool menufmt string menuheaderfmt string menuselectfmt string @@ -274,6 +275,7 @@ func init() { gOpts.info = nil gOpts.infotimefmtnew = "Jan _2 15:04" gOpts.infotimefmtold = "Jan _2 2006" + gOpts.luamsglog = false gOpts.menufmt = "\033[0m" gOpts.menuheaderfmt = "\033[1m" gOpts.menuselectfmt = "\033[7m" diff --git a/parse.go b/parse.go index 6b274f03..feb31ef2 100644 --- a/parse.go +++ b/parse.go @@ -200,6 +200,30 @@ func (e *listExpr) String() string { return buf.String() } +type luaMsgExpr struct { + sourceName string + registry string + msg string + variant string + isAsync bool +} + +func (e *luaMsgExpr) String() string { + return fmt.Sprintf("luamsg: %s, registry: %s, msg: %s, async: %v", e.sourceName, e.registry, e.msg, e.isAsync) +} + +type luaKeyMapExpr struct { + sourceName string + keyMapType string + key string + count int + isAsync bool +} + +func (e *luaKeyMapExpr) String() string { + return fmt.Sprintf("luakeymap: %s, type: %s, key: %q", e.sourceName, e.keyMapType, e.key) +} + type parser struct { scanner *scanner expr expr diff --git a/termseq.go b/termseq.go index 19803b00..d0bf5ff7 100644 --- a/termseq.go +++ b/termseq.go @@ -7,6 +7,7 @@ import ( "unicode/utf8" "github.com/gdamore/tcell/v3" + "github.com/gdamore/tcell/v3/color" ) // gEscapeCode is the byte that starts ANSI control sequences. @@ -277,6 +278,87 @@ func applyOSC(body string, st tcell.Style) tcell.Style { } } +// tcellStyleToString converts a Style object to string +func tcellStyleToString(st tcell.Style) string { + args := []string{} + fg := st.GetForeground() + + addColor := func(c color.Color) { + if c&color.IsRGB == 0 { + args = append(args, "5", strconv.Itoa(int(c&^color.IsValid))) + } else { + r, g, b := c.RGB() + args = append(args, "2", strconv.Itoa(int(r)), strconv.Itoa(int(g)), strconv.Itoa(int(b))) + } + } + + if fg != color.Default { + if fg > color.White { + args = append(args, "38") + addColor(fg) + } else if (fg - color.IsValid) < 8 { + args = append(args, strconv.Itoa(int(30+(fg-color.IsValid)))) + } else { + args = append(args, strconv.Itoa(int(82+(fg-color.IsValid)))) + } + } + + bg := st.GetBackground() + if bg != color.Default { + if bg > color.White { + args = append(args, "48") + addColor(bg) + } else if (bg - color.IsValid) < 8 { + args = append(args, strconv.Itoa(int(40+(bg-color.IsValid)))) + } else { + args = append(args, strconv.Itoa(int(92+(bg-color.IsValid)))) + } + } + + if st.HasBold() { + args = append(args, "1") + } + + if st.HasDim() { + args = append(args, "2") + } + + if st.HasItalic() { + args = append(args, "3") + } + + if st.HasUnderline() { + ulArg := "4" + switch st.GetUnderlineStyle() { + case tcell.UnderlineStyleSolid: + ulArg = "4:1" + case tcell.UnderlineStyleDouble: + ulArg = "4:2" + case tcell.UnderlineStyleCurly: + ulArg = "4:3" + case tcell.UnderlineStyleDotted: + ulArg = "4:4" + case tcell.UnderlineStyleDashed: + ulArg = "4:5" + } + args = append(args, ulArg) + } + + if st.HasBlink() { + args = append(args, "5") + } + + if st.HasReverse() { + args = append(args, "7") + } + + if st.HasStrikeThrough() { + args = append(args, "9") + } + + return "\x1b[" + strings.Join(args, ";") + "m" +} + // Sanitation helpers for untrusted text (filenames, previews, messages). // Pick one of these when handling untrusted input: // diff --git a/ui.go b/ui.go index d55a0666..d9cf0cb9 100644 --- a/ui.go +++ b/ui.go @@ -19,6 +19,7 @@ import ( "github.com/clipperhouse/displaywidth" "github.com/gdamore/tcell/v3" + lua "github.com/yuin/gopher-lua" "golang.org/x/term" ) @@ -250,6 +251,210 @@ type dirStyle struct { role dirRole } +type printDirEntryContext struct { + dir *dir + dirBeg, dirEnd int + dirStyle *dirStyle + + lnwidth int + userWidth, groupWidth, customWidth int + + selections map[string]int + clipboard clipboard + tags map[string]string + visualSelections []string +} + +// printDirEntry draws a single file entry in directory. +func printDirEntry(win *win, screen tcell.Screen, context *printDirEntryContext, i int, f *file) { + dirStyle := context.dirStyle + st := dirStyle.colors.get(f) + + lnwidth := context.lnwidth + indOff, tagOff, nameOff := lnwidth, lnwidth, lnwidth+1 + if !gOpts.mergeindicators { + tagOff++ + nameOff++ + } + + if lnwidth > 0 { + var ln string + pos := context.dir.pos + + if gOpts.number && !gOpts.relativenumber { + ln = fmt.Sprintf("%*d", lnwidth, i+1+context.dirBeg) + } else if gOpts.relativenumber { + switch { + case i < pos: + ln = fmt.Sprintf("%*d", lnwidth, pos-i) + case i > pos: + ln = fmt.Sprintf("%*d", lnwidth, i-pos) + case gOpts.number: + ln = fmt.Sprintf("%-*d", lnwidth, i+1+context.dirBeg) + default: + ln = fmt.Sprintf("%*d", lnwidth, 0) + } + } + + lnDisplay := "" + if i == pos && (getLuaUIFormatter(luaUIFormatterNumberCursor) != nil || gOpts.numbercursorfmt != "") { + lnDisplay = callLuaUIFormatterWithSingleParam(luaUIFormatterNumberCursor, gOpts.numbercursorfmt, ln) + } else { + lnDisplay = callLuaUIFormatterWithSingleParam(luaUIFormatterNumber, gOpts.numberfmt, ln) + } + win.print(screen, 0, i, tcell.StyleDefault, lnDisplay) + } + + path := filepath.Join(context.dir.path, f.Name()) + + drawFileState := false + var fileStateStyle tcell.Style + if slices.Contains(context.visualSelections, path) { + drawFileState = true + fileStateStyle = getLuaUIStyleWithDefaultStr(luaUIStyleVisual, gOpts.visualfmt) + } else if _, ok := context.selections[path]; ok { + drawFileState = true + fileStateStyle = getLuaUIStyleWithDefaultStr(luaUIStyleSelect, gOpts.selectfmt) + } else if slices.Contains(context.clipboard.paths, path) { + drawFileState = true + if context.clipboard.mode == clipboardCopy { + fileStateStyle = getLuaUIStyleWithDefaultStr(luaUIStyleCopy, gOpts.copyfmt) + } else { + fileStateStyle = getLuaUIStyleWithDefaultStr(luaUIStyleCut, gOpts.cutfmt) + } + } + + tag := " " + if val, ok := context.tags[path]; ok && len(val) > 0 { + tag = val + } + + if drawFileState { + ind := " " + if gOpts.mergeindicators { + ind = tag + } + win.print(screen, indOff, i, fileStateStyle, ind) + } + + // make space for select marker, and leave another space at the end + maxWidth := win.w - lnwidth - 2 + + var icon string + var iconDef iconDef + if gOpts.icons { + iconDef = dirStyle.icons.get(f) + icon = iconDef.icon + " " + } + + // subtract space for icon + maxFilenameWidth := maxWidth - displaywidth.String(icon) + // subtract space for tag if not merged with selection marker + if !gOpts.mergeindicators { + maxFilenameWidth-- + } + + info, custom, customOff := fileInfo(f, context.dir, context.userWidth, context.groupWidth, context.customWidth) + infolen := len(info) + showInfo := infolen > 0 && 2*infolen < maxWidth + if showInfo { + maxFilenameWidth -= infolen + } + + filename := truncateFilename(f, maxFilenameWidth, gOpts.truncatepct, gOpts.truncatechar) + spacing := maxFilenameWidth - displaywidth.String(filename) + if spacing > 0 { + filename += strings.Repeat(" ", spacing) + } + + if showInfo { + filename += info + customOff += nameOff + displaywidth.String(icon) + maxFilenameWidth + } + + if i == context.dir.pos { + var cursorFmt string + var luaFormatterName string + switch dirStyle.role { + case Active: + cursorFmt = optionToFmtstr(gOpts.cursoractivefmt) + luaFormatterName = luaUIFormatterCursorActive + case Parent: + cursorFmt = optionToFmtstr(gOpts.cursorparentfmt) + luaFormatterName = luaUIFormatterCursorParent + case Preview: + cursorFmt = optionToFmtstr(gOpts.cursorpreviewfmt) + luaFormatterName = luaUIFormatterCursorPreview + } + + // print tag separately as it can contain color escape sequences + if !gOpts.mergeindicators || !drawFileState { + tagStr := callLuaUIFormatterWithSingleParam(luaFormatterName, cursorFmt, tag) + win.print(screen, tagOff, i, st, tagStr) + } + + fileEntryStr := callLuaUIFormatterWithSingleParam(luaFormatterName, cursorFmt, icon+filename+" ") + win.print(screen, nameOff, i, st, fileEntryStr) + + // print over the empty space we reserved for the custom info + if showInfo && custom != "" { + customStr := callLuaUIFormatterWithSingleParam(luaFormatterName, cursorFmt, stripTermSequence(custom)) + win.print(screen, customOff, i, st, customStr) + } + } else { + if !gOpts.mergeindicators || !drawFileState { + if tag == " " { + win.print(screen, tagOff, i, st, " ") + } else { + tagStr := callLuaUIFormatterWithSingleParam(luaUIFormatterTag, gOpts.tagfmt, tag) + win.print(screen, tagOff, i, tcell.StyleDefault, tagStr) + } + } + + if len(icon) > 0 { + iconStyle := st + if iconDef.hasStyle { + iconStyle = iconDef.style + } + win.print(screen, nameOff, i, iconStyle, icon) + } + + win.print(screen, nameOff+displaywidth.String(icon), i, st, filename+" ") + + // print over the empty space we reserved for the custom info + if showInfo && custom != "" { + win.print(screen, customOff, i, st, custom) + } + } +} + +func tryPrintDirEntriesWithLua(win *win, ui *ui, context *printDirEntryContext, files []*file) bool { + msgExpr := getLuaUIPrinter(luaUIPrinterDirEntry) + if msgExpr == nil { + return false + } + + for i, f := range files { + _, err := callLuaMsgExpr(msgExpr, func(L *lua.LState) []lua.LValue { + data := L.NewTable() + data.RawSetString("context", lWrapPrintDirEntryContext(L, context)) + data.RawSetString("index", lua.LNumber(i)) + data.RawSetString("file", lWrapFile(L, f)) + + return []lua.LValue{ + lWrapWin(L, win), + lWrapUI(L, ui), + data, + } + }) + if err != nil { + log.Printf("Lua UI printer %s error: %s", luaUIPrinterDirEntry, err) + } + } + + return true +} + func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirStyle, previewTimer *time.Timer) { if win.w < 5 || dir == nil { return @@ -312,152 +517,59 @@ func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirSty } } - indOff, tagOff, nameOff := lnwidth, lnwidth, lnwidth+1 - if !gOpts.mergeindicators { - tagOff++ - nameOff++ - } - visualSelections := dir.visualSelections() - for i, f := range dir.files[beg:end] { - st := dirStyle.colors.get(f) - if lnwidth > 0 { - var ln string + entryContext := printDirEntryContext{ + dir: dir, + dirBeg: beg, + dirEnd: end, + dirStyle: dirStyle, - if gOpts.number && !gOpts.relativenumber { - ln = fmt.Sprintf("%*d", lnwidth, i+1+beg) - } else if gOpts.relativenumber { - switch { - case i < dir.pos: - ln = fmt.Sprintf("%*d", lnwidth, dir.pos-i) - case i > dir.pos: - ln = fmt.Sprintf("%*d", lnwidth, i-dir.pos) - case gOpts.number: - ln = fmt.Sprintf("%-*d", lnwidth, i+1+beg) - default: - ln = fmt.Sprintf("%*d", lnwidth, 0) - } - } - - fmtStr := optionToFmtstr(gOpts.numberfmt) - if i == dir.pos && gOpts.numbercursorfmt != "" { - fmtStr = optionToFmtstr(gOpts.numbercursorfmt) - } - win.print(ui.screen, 0, i, tcell.StyleDefault, fmt.Sprintf(fmtStr, ln)) - } - - path := filepath.Join(dir.path, f.Name()) + lnwidth: lnwidth, + userWidth: userWidth, + groupWidth: groupWidth, + customWidth: customWidth, - var fmtStr string - if slices.Contains(visualSelections, path) { - fmtStr = gOpts.visualfmt - } else if _, ok := context.selections[path]; ok { - fmtStr = gOpts.selectfmt - } else if slices.Contains(context.clipboard.paths, path) { - if context.clipboard.mode == clipboardCopy { - fmtStr = gOpts.copyfmt - } else { - fmtStr = gOpts.cutfmt - } - } - - tag := " " - if val, ok := context.tags[path]; ok && len(val) > 0 { - tag = val - } - - if fmtStr != "" { - ind := " " - if gOpts.mergeindicators { - ind = tag - } - win.print(ui.screen, indOff, i, parseEscapeSequence(fmtStr), ind) - } + selections: context.selections, + clipboard: context.clipboard, + tags: context.tags, + visualSelections: visualSelections, + } - // make space for select marker, and leave another space at the end - maxWidth := win.w - lnwidth - 2 + files := dir.files[beg:end] - var icon string - var iconDef iconDef - if gOpts.icons { - iconDef = dirStyle.icons.get(f) - icon = iconDef.icon + " " - } - - // subtract space for icon - maxFilenameWidth := maxWidth - displaywidth.String(icon) - // subtract space for tag if not merged with selection marker - if !gOpts.mergeindicators { - maxFilenameWidth-- + if !tryPrintDirEntriesWithLua(win, ui, &entryContext, files) { + for i, f := range files { + printDirEntry(win, ui.screen, &entryContext, i, f) } + } +} - info, custom, customOff := fileInfo(f, dir, userWidth, groupWidth, customWidth) - infolen := len(info) - showInfo := infolen > 0 && 2*infolen < maxWidth - if showInfo { - maxFilenameWidth -= infolen - } +func (win *win) tryPrintDirWithLua(ui *ui, dir *dir, context *dirContext, dirStyle *dirStyle, previewTimer *time.Timer) bool { + msgExpr := getLuaUIPrinter(luaUIPrinterDirectory) + if msgExpr == nil { + return false + } - filename := truncateFilename(f, maxFilenameWidth, gOpts.truncatepct, gOpts.truncatechar) - spacing := maxFilenameWidth - displaywidth.String(filename) - if spacing > 0 { - filename += strings.Repeat(" ", spacing) - } + _, err := callLuaMsgExpr(msgExpr, func(L *lua.LState) []lua.LValue { + args := L.NewTable() + args.RawSetString("dir", lWrapDir(L, dir)) + args.RawSetString("context", lWrapDirContext(L, context)) + args.RawSetString("dir_style", lWrapDirStyle(L, dirStyle)) + args.RawSetString("preview_timer", lWrapTimer(L, previewTimer)) - if showInfo { - filename += info - customOff += nameOff + displaywidth.String(icon) + maxFilenameWidth + return []lua.LValue{ + lWrapWin(L, win), + lWrapUI(L, ui), + args, } + }) - if i == dir.pos { - var cursorFmt string - switch dirStyle.role { - case Active: - cursorFmt = optionToFmtstr(gOpts.cursoractivefmt) - case Parent: - cursorFmt = optionToFmtstr(gOpts.cursorparentfmt) - case Preview: - cursorFmt = optionToFmtstr(gOpts.cursorpreviewfmt) - } - - // print tag separately as it can contain color escape sequences - if !gOpts.mergeindicators || fmtStr == "" { - win.print(ui.screen, tagOff, i, st, fmt.Sprintf(cursorFmt, tag)) - } - - win.print(ui.screen, nameOff, i, st, fmt.Sprintf(cursorFmt, icon+filename+" ")) - - // print over the empty space we reserved for the custom info - if showInfo && custom != "" { - win.print(ui.screen, customOff, i, st, fmt.Sprintf(cursorFmt, stripTermSequence(custom))) - } - } else { - if !gOpts.mergeindicators || fmtStr == "" { - if tag == " " { - win.print(ui.screen, tagOff, i, st, " ") - } else { - tagStr := fmt.Sprintf(optionToFmtstr(gOpts.tagfmt), tag) - win.print(ui.screen, tagOff, i, tcell.StyleDefault, tagStr) - } - } - - if len(icon) > 0 { - iconStyle := st - if iconDef.hasStyle { - iconStyle = iconDef.style - } - win.print(ui.screen, nameOff, i, iconStyle, icon) - } - - win.print(ui.screen, nameOff+displaywidth.String(icon), i, st, filename+" ") - - // print over the empty space we reserved for the custom info - if showInfo && custom != "" { - win.print(ui.screen, customOff, i, st, custom) - } - } + if err != nil { + log.Printf("Lua printer %s error: %s", luaUIPrinterDirectory, err) } + + return true } func getUserWidth(dir *dir, beg, end int) int { @@ -591,7 +703,7 @@ func (ui *ui) echomsg(msg string) { } func (ui *ui) echoerr(msg string) { - ui.echo(fmt.Sprintf(optionToFmtstr(gOpts.errorfmt), sanitizeName(msg))) + ui.echo(formatDisplayedErrorMsg(msg)) log.Printf("error: %s", msg) } @@ -702,6 +814,58 @@ func (ui *ui) drawPromptLine(nav *nav) { ui.promptWin.print(ui.screen, 0, 0, st, prompt) } +func (ui *ui) tryDrawPromptLineWithLua(nav *nav) bool { + msgExpr := getLuaUIPrinter(luaUIPrinterPrompt) + if msgExpr == nil { + return false + } + + _, err := callLuaMsgExpr(msgExpr, func(L *lua.LState) []lua.LValue { + dir := nav.currDir() + pwd := sanitizeName(dir.path) + + if after, ok := strings.CutPrefix(pwd, gUser.HomeDir); ok { + pwd = filepath.Join("~", after) + } + + var fname string + if curr := nav.currFile(); curr != nil { + fname = sanitizeName(filepath.Base(curr.path)) + } + + pwdWithSep := pwd + sep := string(filepath.Separator) + if !strings.HasSuffix(pwd, sep) { + pwdWithSep += sep + } + + filter := L.NewTable() + for _, f := range dir.filter { + filter.Append(lua.LString(f)) + } + + data := L.NewTable() + data.RawSetString("user_name", lua.LString(gUser.Username)) + data.RawSetString("host_name", lua.LString(gHostname)) + data.RawSetString("file_name", lua.LString(fname)) + data.RawSetString("pwd", lua.LString(pwd)) + data.RawSetString("pwd_with_sep", lua.LString(pwdWithSep)) + data.RawSetString("filter", filter) + + return []lua.LValue{ + lWrapWin(L, ui.promptWin), + lWrapUI(L, ui), + data, + } + }) + + if err != nil { + log.Printf("Lua printer %s error: %s", luaUIPrinterPrompt, err) + } + + return true +} + // Deprecated: Only called by drawRuler, which will eventually be replaced by drawRulerFile func formatRulerOpt(name, val string) string { // handle escape character so it doesn't mess up the ruler @@ -763,7 +927,9 @@ func (ui *ui) drawStat(nav *nav) { } } - ui.msgWin.print(ui.screen, 0, 0, tcell.StyleDefault, fileInfo.String()) + fileInfoStr := fileInfo.String() + + ui.msgWin.print(ui.screen, 0, 0, tcell.StyleDefault, fileInfoStr) } // Deprecated: Will eventually be replaced by drawRulerFile @@ -880,7 +1046,7 @@ func (ui *ui) drawRuler(nav *nav) { func (ui *ui) drawRulerFile(nav *nav) { if ui.rulerErr != nil { - err := fmt.Sprintf(optionToFmtstr(gOpts.errorfmt), sanitizeName(fmt.Sprintf("parsing ruler: %s", ui.rulerErr))) + err := formatDisplayedErrorMsg(fmt.Sprintf("parsing ruler: %s", ui.rulerErr)) ui.msgWin.print(ui.screen, 0, 0, tcell.StyleDefault, err) return } @@ -1009,7 +1175,7 @@ func (ui *ui) drawRulerFile(nav *nav) { left, right, err := renderRuler(ui.ruler, data, ui.msgWin.w) if err != nil { - err := fmt.Sprintf(optionToFmtstr(gOpts.errorfmt), sanitizeName(fmt.Sprintf("rendering ruler: %s", err))) + err := formatDisplayedErrorMsg(fmt.Sprintf("rendering ruler: %s", err)) ui.msgWin.print(ui.screen, 0, 0, tcell.StyleDefault, err) return } @@ -1018,6 +1184,26 @@ func (ui *ui) drawRulerFile(nav *nav) { ui.msgWin.printRight(ui.screen, 0, tcell.StyleDefault, right) } +func (ui *ui) tryDrawRulerWithLua(nav *nav) bool { + msgExpr := getLuaUIPrinter(luaUIPrinterRuler) + if msgExpr == nil { + return false + } + + _, err := callLuaMsgExpr(msgExpr, func(L *lua.LState) []lua.LValue { + return []lua.LValue{ + lWrapWin(L, ui.msgWin), + lWrapUI(L, ui), + } + }) + + if err != nil { + log.Printf("Lua printer %s error: %s", luaUIPrinterRuler, err) + } + + return true +} + func (ui *ui) drawPreview(nav *nav, context *dirContext) { curr := nav.currFile() if curr == nil { @@ -1036,13 +1222,16 @@ func (ui *ui) drawPreview(nav *nav, context *dirContext) { ui.sxScreen.lastFile = "" dir := nav.getDir(curr.path) dirStyle := &dirStyle{colors: ui.styles, icons: ui.icons, role: Preview} - win.printDir(ui, dir, context, dirStyle, nav.previewTimer) + + if !win.tryPrintDirWithLua(ui, dir, context, dirStyle, nav.previewTimer) { + win.printDir(ui, dir, context, dirStyle, nav.previewTimer) + } } } } func (ui *ui) drawBox() { - st := parseEscapeSequence(gOpts.borderfmt) + st := getLuaUIStyleWithDefaultStr(luaUIStyleBorder, gOpts.borderfmt) w, h := ui.screen.Size() style := gOpts.borderstyle @@ -1112,16 +1301,16 @@ func (ui *ui) drawMenu() { for i, line := range lines { var st tcell.Style if i == 0 { - st = parseEscapeSequence(gOpts.menuheaderfmt) + st = getLuaUIStyleWithDefaultStr(luaUIStyleMenuheader, gOpts.menuheaderfmt) } else { - st = parseEscapeSequence(gOpts.menufmt) + st = getLuaUIStyleWithDefaultStr(luaUIStyleMenu, gOpts.menufmt) } ui.menuWin.printLine(ui.screen, 0, i, st, line) } if ui.menuSelect != nil { - st := parseEscapeSequence(gOpts.menuselectfmt) + st := getLuaUIStyleWithDefaultStr(luaUIStyleMenuselect, gOpts.menuselectfmt) ui.menuWin.print(ui.screen, ui.menuSelect.x, ui.menuSelect.y, st, ui.menuSelect.s) } } @@ -1144,7 +1333,9 @@ func (ui *ui) draw(nav *nav) { ui.screen.Clear() - ui.drawPromptLine(nav) + if !ui.tryDrawPromptLineWithLua(nav) { + ui.drawPromptLine(nav) + } wins := len(ui.wins) if gOpts.preview { @@ -1157,7 +1348,10 @@ func (ui *ui) draw(nav *nav) { } if dir := ui.dirOfWin(nav, i); dir != nil { dirStyle := &dirStyle{colors: ui.styles, icons: ui.icons, role: role} - ui.wins[i].printDir(ui, dir, &context, dirStyle, nav.previewTimer) + + if !ui.wins[i].tryPrintDirWithLua(ui, dir, &context, dirStyle, nav.previewTimer) { + ui.wins[i].printDir(ui, dir, &context, dirStyle, nav.previewTimer) + } } } @@ -1168,7 +1362,9 @@ func (ui *ui) draw(nav *nav) { switch ui.cmdPrefix { case "": - if gOpts.rulerfmt == "" { + if ui.tryDrawRulerWithLua(nav) { + // pass + } else if gOpts.rulerfmt == "" { ui.drawRulerFile(nav) } else { ui.drawStat(nav) @@ -1428,6 +1624,14 @@ func (ui *ui) readNormalEvent(ev tcell.Event, nav *nav) expr { expr = &callExpr{name: e.name, args: e.args, count: count} case *listExpr: expr = &listExpr{exprs: e.exprs, count: count} + case *luaKeyMapExpr: + expr = &luaKeyMapExpr{ + sourceName: e.sourceName, + keyMapType: e.keyMapType, + key: e.key, + count: count, + isAsync: e.isAsync, + } } } }