mirror of
https://github.com/zoriya/jjui.git
synced 2026-08-16 02:44:50 +00:00
* refactor: move `helpkeys` into `help` * refactor: replace owner-based dispatch with scope-layer routing Move UI action dispatch to explicit routing layers exposed by each model instead of special-casing owner-based paths in ui.Model This also tightens dispatch behaviour for blocking views so cancel/esc and stacked overlays route consistently. * refactor(revisions): activate revision operations * refactor(quick_search): use consistent naming * refactor: rename bindings.Scope * refactor: rename Layer to Scope * refactor: move `routing` to `dispatch` * fix: AllowLeak * refactor: add `VisibleScopes` * refactor: move help building to `status` * feat(revisions): stacked operations * fix(revisions): tighten architecture * refactor: change `AllowLeak` to a policy * feat(status): group bindings by scopes * feat(status): mark overriden keybindings with strikethrough
705 lines
18 KiB
Go
705 lines
18 KiB
Go
package scripting
|
|
|
|
import (
|
|
stdcontext "context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
"github.com/atotto/clipboard"
|
|
"github.com/idursun/jjui/internal/ui/actionmeta"
|
|
"github.com/idursun/jjui/internal/ui/choose"
|
|
"github.com/idursun/jjui/internal/ui/common"
|
|
uicontext "github.com/idursun/jjui/internal/ui/context"
|
|
"github.com/idursun/jjui/internal/ui/exec_process"
|
|
"github.com/idursun/jjui/internal/ui/input"
|
|
"github.com/idursun/jjui/internal/ui/intents"
|
|
"github.com/idursun/jjui/internal/ui/revisions"
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
type step struct {
|
|
cmd tea.Cmd
|
|
matcher func(tea.Msg) (bool, []lua.LValue)
|
|
}
|
|
|
|
type Runner struct {
|
|
ctx *uicontext.MainContext
|
|
main *lua.LState
|
|
thread *lua.LState
|
|
cancel stdcontext.CancelFunc
|
|
fn *lua.LFunction
|
|
started bool
|
|
await func(tea.Msg) (bool, []lua.LValue)
|
|
resumeArgs []lua.LValue
|
|
done bool
|
|
}
|
|
|
|
func RunScript(ctx *uicontext.MainContext, src string) (*Runner, tea.Cmd, error) {
|
|
L, err := vmFromContext(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
r := &Runner{ctx: ctx, main: L}
|
|
|
|
fn, err := L.LoadString(src)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("lua: %w", err)
|
|
}
|
|
r.fn = fn
|
|
r.thread, r.cancel = L.NewThread()
|
|
|
|
cmd := r.resume()
|
|
if r.done {
|
|
r.close()
|
|
}
|
|
return r, cmd, nil
|
|
}
|
|
|
|
func (r *Runner) close() {
|
|
if r.cancel != nil {
|
|
r.cancel()
|
|
r.cancel = nil
|
|
}
|
|
}
|
|
|
|
func (r *Runner) resume() tea.Cmd {
|
|
if r.done {
|
|
return nil
|
|
}
|
|
var cmds []tea.Cmd
|
|
for {
|
|
var fn *lua.LFunction
|
|
if !r.started {
|
|
fn = r.fn
|
|
}
|
|
args := r.resumeArgs
|
|
r.resumeArgs = nil
|
|
state, err, values := r.main.Resume(r.thread, fn, args...)
|
|
r.started = true
|
|
if err != nil {
|
|
r.done = true
|
|
cmds = append(cmds, intents.Invoke(intents.AddMessage{Text: err.Error(), Err: err}))
|
|
break
|
|
}
|
|
for _, v := range values {
|
|
if ud, ok := v.(*lua.LUserData); ok {
|
|
if st, ok := ud.Value.(step); ok {
|
|
if st.matcher != nil {
|
|
r.await = st.matcher
|
|
if st.cmd != nil {
|
|
cmds = append(cmds, st.cmd)
|
|
}
|
|
return tea.Sequence(cmds...)
|
|
}
|
|
if st.cmd != nil {
|
|
cmds = append(cmds, st.cmd)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if state == lua.ResumeOK {
|
|
r.done = true
|
|
break
|
|
}
|
|
// continue to resume to collect subsequent steps until an await or completion
|
|
if len(cmds) > 0 {
|
|
continue
|
|
}
|
|
}
|
|
if len(cmds) == 0 {
|
|
return nil
|
|
}
|
|
return tea.Sequence(cmds...)
|
|
}
|
|
|
|
// HandleMsg resumes the script if waiting for a matching message.
|
|
func (r *Runner) HandleMsg(msg tea.Msg) tea.Cmd {
|
|
if r.await == nil {
|
|
return nil
|
|
}
|
|
ok, resume := r.await(msg)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
r.await = nil
|
|
r.resumeArgs = resume
|
|
cmd := r.resume()
|
|
if r.done {
|
|
r.close()
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func (r *Runner) Done() bool {
|
|
return r.done && r.await == nil
|
|
}
|
|
|
|
func registerAPI(L *lua.LState, ctx *uicontext.MainContext) {
|
|
revisionsTable := L.NewTable()
|
|
revisionsTable.RawSetString("current", L.NewFunction(func(L *lua.LState) int {
|
|
if rev, ok := ctx.SelectedItem.(uicontext.SelectedRevision); ok {
|
|
L.Push(lua.LString(rev.ChangeId))
|
|
return 1
|
|
}
|
|
return 0
|
|
}))
|
|
revisionsTable.RawSetString("checked", L.NewFunction(func(L *lua.LState) int {
|
|
tbl := L.NewTable()
|
|
for _, item := range ctx.CheckedItems {
|
|
if rev, ok := item.(uicontext.SelectedRevision); ok {
|
|
tbl.Append(lua.LString(rev.ChangeId))
|
|
}
|
|
}
|
|
L.Push(tbl)
|
|
return 1
|
|
}))
|
|
revisionsTable.RawSetString("refresh", L.NewFunction(func(L *lua.LState) int {
|
|
payload := payloadFromTop(L)
|
|
intent := intents.Refresh{
|
|
KeepSelections: boolVal(payload, "keep_selections"),
|
|
SelectedRevision: stringVal(payload, "selected_revision"),
|
|
}
|
|
return yieldStep(L, step{cmd: revisions.RevisionsCmd(intent), matcher: matchUpdateRevisionsSuccess})
|
|
}))
|
|
revisionsTable.RawSetString("navigate", L.NewFunction(func(L *lua.LState) int {
|
|
payload := payloadFromTop(L)
|
|
target := parseNavigateTarget(stringVal(payload, "target"))
|
|
intent := intents.Navigate{
|
|
Delta: intVal(payload, "by"),
|
|
IsPage: boolVal(payload, "page"),
|
|
Target: target,
|
|
ChangeID: stringVal(payload, "to"),
|
|
FallbackID: stringVal(payload, "fallback"),
|
|
}
|
|
if v, ok := payload["ensureView"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
intent.EnsureView = boolPtr(b)
|
|
}
|
|
}
|
|
if v, ok := payload["allowStream"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
intent.AllowStream = boolPtr(b)
|
|
}
|
|
}
|
|
return yieldStep(L, step{cmd: revisions.RevisionsCmd(intent)})
|
|
}))
|
|
|
|
revsetTable := L.NewTable()
|
|
revsetTable.RawSetString("current", L.NewFunction(func(L *lua.LState) int {
|
|
L.Push(lua.LString(ctx.CurrentRevset))
|
|
return 1
|
|
}))
|
|
revsetTable.RawSetString("default", L.NewFunction(func(L *lua.LState) int {
|
|
L.Push(lua.LString(ctx.DefaultRevset))
|
|
return 1
|
|
}))
|
|
|
|
contextTable := L.NewTable()
|
|
contextTable.RawSetString("change_id", L.NewFunction(func(L *lua.LState) int {
|
|
switch item := ctx.SelectedItem.(type) {
|
|
case uicontext.SelectedRevision:
|
|
L.Push(lua.LString(item.ChangeId))
|
|
return 1
|
|
case uicontext.SelectedFile:
|
|
L.Push(lua.LString(item.ChangeId))
|
|
return 1
|
|
}
|
|
return 0
|
|
}))
|
|
contextTable.RawSetString("commit_id", L.NewFunction(func(L *lua.LState) int {
|
|
switch item := ctx.SelectedItem.(type) {
|
|
case uicontext.SelectedRevision:
|
|
L.Push(lua.LString(item.CommitId))
|
|
return 1
|
|
case uicontext.SelectedFile:
|
|
L.Push(lua.LString(item.CommitId))
|
|
return 1
|
|
case uicontext.SelectedCommit:
|
|
L.Push(lua.LString(item.CommitId))
|
|
return 1
|
|
}
|
|
return 0
|
|
}))
|
|
contextTable.RawSetString("file", L.NewFunction(func(L *lua.LState) int {
|
|
if item, ok := ctx.SelectedItem.(uicontext.SelectedFile); ok {
|
|
L.Push(lua.LString(item.File))
|
|
return 1
|
|
}
|
|
return 0
|
|
}))
|
|
contextTable.RawSetString("operation_id", L.NewFunction(func(L *lua.LState) int {
|
|
if item, ok := ctx.SelectedItem.(uicontext.SelectedOperation); ok {
|
|
L.Push(lua.LString(item.OperationId))
|
|
return 1
|
|
}
|
|
return 0
|
|
}))
|
|
contextTable.RawSetString("checked_files", L.NewFunction(func(L *lua.LState) int {
|
|
tbl := L.NewTable()
|
|
for _, item := range ctx.CheckedItems {
|
|
if f, ok := item.(uicontext.SelectedFile); ok {
|
|
tbl.Append(lua.LString(f.File))
|
|
}
|
|
}
|
|
L.Push(tbl)
|
|
return 1
|
|
}))
|
|
contextTable.RawSetString("checked_change_ids", L.NewFunction(func(L *lua.LState) int {
|
|
tbl := L.NewTable()
|
|
for _, item := range ctx.CheckedItems {
|
|
switch i := item.(type) {
|
|
case uicontext.SelectedRevision:
|
|
tbl.Append(lua.LString(i.ChangeId))
|
|
case uicontext.SelectedFile:
|
|
tbl.Append(lua.LString(i.ChangeId))
|
|
}
|
|
}
|
|
L.Push(tbl)
|
|
return 1
|
|
}))
|
|
contextTable.RawSetString("checked_commit_ids", L.NewFunction(func(L *lua.LState) int {
|
|
tbl := L.NewTable()
|
|
for _, item := range ctx.CheckedItems {
|
|
switch i := item.(type) {
|
|
case uicontext.SelectedRevision:
|
|
tbl.Append(lua.LString(i.CommitId))
|
|
case uicontext.SelectedFile:
|
|
tbl.Append(lua.LString(i.CommitId))
|
|
case uicontext.SelectedCommit:
|
|
tbl.Append(lua.LString(i.CommitId))
|
|
}
|
|
}
|
|
L.Push(tbl)
|
|
return 1
|
|
}))
|
|
|
|
jjAsyncFn := L.NewFunction(func(L *lua.LState) int {
|
|
args := argsFromLua(L)
|
|
return yieldStep(L, step{cmd: ctx.RunCommand(args)})
|
|
})
|
|
jjInteractiveFn := L.NewFunction(func(L *lua.LState) int {
|
|
args := argsFromLua(L)
|
|
return yieldStep(L, step{cmd: ctx.RunInteractiveCommand(args, nil)})
|
|
})
|
|
jjFn := L.NewFunction(func(L *lua.LState) int {
|
|
args := argsFromLua(L)
|
|
out, err := ctx.RunCommandImmediate(args)
|
|
if err != nil {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString(err.Error()))
|
|
return 2
|
|
}
|
|
L.Push(lua.LString(out))
|
|
L.Push(lua.LNil)
|
|
return 2
|
|
})
|
|
flashFn := L.NewFunction(func(L *lua.LState) int {
|
|
intent := intents.AddMessage{}
|
|
switch v := L.Get(1).(type) {
|
|
case *lua.LTable:
|
|
payload := luaTableToMap(v)
|
|
intent.Text = stringVal(payload, "text")
|
|
if boolVal(payload, "error") {
|
|
intent.Err = fmt.Errorf("%s", intent.Text)
|
|
}
|
|
intent.Sticky = boolVal(payload, "sticky")
|
|
default:
|
|
intent.Text = L.CheckString(1)
|
|
}
|
|
return yieldStep(L, step{cmd: intents.Invoke(intent)})
|
|
})
|
|
copyToClipboardFn := L.NewFunction(func(L *lua.LState) int {
|
|
text := L.CheckString(1)
|
|
if err := clipboard.WriteAll(text); err != nil {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString(err.Error()))
|
|
return 2
|
|
}
|
|
L.Push(lua.LBool(true))
|
|
L.Push(lua.LNil)
|
|
return 2
|
|
})
|
|
execShellFn := L.NewFunction(func(L *lua.LState) int {
|
|
command := L.CheckString(1)
|
|
msg := common.ExecMsg{
|
|
Line: command,
|
|
Mode: common.ExecShell,
|
|
}
|
|
return yieldStep(L, step{cmd: exec_process.ExecLine(ctx, msg)})
|
|
})
|
|
splitLinesFn := L.NewFunction(func(L *lua.LState) int {
|
|
text := L.CheckString(1)
|
|
keepEmpty := false
|
|
if L.GetTop() >= 2 {
|
|
keepEmpty = L.CheckBool(2)
|
|
}
|
|
tbl := L.NewTable()
|
|
for line := range strings.SplitSeq(text, "\n") {
|
|
line = strings.TrimSuffix(line, "\r")
|
|
if line == "" && !keepEmpty {
|
|
continue
|
|
}
|
|
tbl.Append(lua.LString(line))
|
|
}
|
|
L.Push(tbl)
|
|
return 1
|
|
})
|
|
chooseFn := L.NewFunction(func(L *lua.LState) int {
|
|
var (
|
|
options []string
|
|
title string
|
|
filter bool
|
|
ordered bool
|
|
)
|
|
if L.GetTop() == 1 {
|
|
if tbl, ok := L.Get(1).(*lua.LTable); ok {
|
|
if optVal := tbl.RawGetString("options"); optVal != lua.LNil {
|
|
if optTbl, ok := optVal.(*lua.LTable); ok {
|
|
options = stringSliceFromTable(optTbl)
|
|
} else if s, ok := optVal.(lua.LString); ok {
|
|
options = []string{s.String()}
|
|
}
|
|
}
|
|
if titleVal := tbl.RawGetString("title"); titleVal != lua.LNil {
|
|
title = titleVal.String()
|
|
}
|
|
if filterVal := tbl.RawGetString("filter"); filterVal != lua.LNil {
|
|
filter = bool(filterVal.(lua.LBool))
|
|
}
|
|
if orderedVal := tbl.RawGetString("ordered"); orderedVal != lua.LNil {
|
|
ordered = bool(orderedVal.(lua.LBool))
|
|
}
|
|
if options == nil {
|
|
options = stringSliceFromTable(tbl)
|
|
}
|
|
return yieldStep(L, step{cmd: choose.ShowOrdered(options, title, filter, ordered), matcher: matchChoose})
|
|
}
|
|
}
|
|
options = argsFromLua(L)
|
|
return yieldStep(L, step{cmd: choose.ShowWithTitle(options, "", false), matcher: matchChoose})
|
|
})
|
|
inputFn := L.NewFunction(func(L *lua.LState) int {
|
|
var title, prompt string
|
|
if L.GetTop() == 1 {
|
|
if tbl, ok := L.Get(1).(*lua.LTable); ok {
|
|
if titleVal := tbl.RawGetString("title"); titleVal != lua.LNil {
|
|
title = titleVal.String()
|
|
}
|
|
if promptVal := tbl.RawGetString("prompt"); promptVal != lua.LNil {
|
|
prompt = promptVal.String()
|
|
}
|
|
return yieldStep(L, step{cmd: input.ShowWithTitle(title, prompt), matcher: matchInput})
|
|
}
|
|
}
|
|
return yieldStep(L, step{cmd: input.ShowWithTitle("", ""), matcher: matchInput})
|
|
})
|
|
waitCloseFn := L.NewFunction(func(L *lua.LState) int {
|
|
return yieldStep(L, step{matcher: matchCloseViewMsg})
|
|
})
|
|
waitRefreshFn := L.NewFunction(func(L *lua.LState) int {
|
|
return yieldStep(L, step{matcher: matchUpdateRevisionsSuccess})
|
|
})
|
|
|
|
// make sure we have a `jjui` namespace
|
|
root := L.NewTable()
|
|
root.RawSetString("revisions", revisionsTable)
|
|
root.RawSetString("revset", revsetTable)
|
|
root.RawSetString("context", contextTable)
|
|
root.RawSetString("jj_async", jjAsyncFn)
|
|
root.RawSetString("jj_interactive", jjInteractiveFn)
|
|
root.RawSetString("jj", jjFn)
|
|
root.RawSetString("flash", flashFn)
|
|
root.RawSetString("copy_to_clipboard", copyToClipboardFn)
|
|
root.RawSetString("exec_shell", execShellFn)
|
|
root.RawSetString("split_lines", splitLinesFn)
|
|
root.RawSetString("choose", chooseFn)
|
|
root.RawSetString("input", inputFn)
|
|
root.RawSetString("wait_close", waitCloseFn)
|
|
root.RawSetString("wait_refresh", waitRefreshFn)
|
|
builtinRoot := L.NewTable()
|
|
root.RawSetString("builtin", builtinRoot)
|
|
registerGeneratedActionAPI(L, root, false)
|
|
registerGeneratedActionAPI(L, builtinRoot, true)
|
|
L.SetGlobal("jjui", root)
|
|
|
|
// but also expose at the top level for convenience
|
|
L.SetGlobal("revisions", revisionsTable)
|
|
L.SetGlobal("revset", revsetTable)
|
|
if diffTable, ok := root.RawGetString("diff").(*lua.LTable); ok {
|
|
L.SetGlobal("diff", diffTable)
|
|
}
|
|
if uiTable, ok := root.RawGetString("ui").(*lua.LTable); ok {
|
|
L.SetGlobal("ui", uiTable)
|
|
}
|
|
L.SetGlobal("context", contextTable)
|
|
L.SetGlobal("jj_async", jjAsyncFn)
|
|
L.SetGlobal("jj_interactive", jjInteractiveFn)
|
|
L.SetGlobal("jj", jjFn)
|
|
L.SetGlobal("flash", flashFn)
|
|
L.SetGlobal("copy_to_clipboard", copyToClipboardFn)
|
|
L.SetGlobal("exec_shell", execShellFn)
|
|
L.SetGlobal("split_lines", splitLinesFn)
|
|
L.SetGlobal("choose", chooseFn)
|
|
L.SetGlobal("input", inputFn)
|
|
L.SetGlobal("wait_close", waitCloseFn)
|
|
L.SetGlobal("wait_refresh", waitRefreshFn)
|
|
}
|
|
|
|
func registerGeneratedActionAPI(L *lua.LState, root *lua.LTable, builtIn bool) {
|
|
actions := actionmeta.BuiltInActions()
|
|
for _, actionID := range actions {
|
|
scopes := actionmeta.ActionScopes(actionID)
|
|
for _, scope := range scopes {
|
|
scopeTable := ensureScopeTable(L, root, scope)
|
|
token := actionTokenFromCanonical(actionID)
|
|
if token == "" {
|
|
continue
|
|
}
|
|
// Keep existing utility helpers (e.g. jjui.revisions.refresh) intact.
|
|
if scopeTable.RawGetString(token) != lua.LNil {
|
|
continue
|
|
}
|
|
scopeTable.RawSetString(token, generatedActionFn(L, actionID, builtIn))
|
|
if token == "cancel" && scopeTable.RawGetString("close") == lua.LNil {
|
|
scopeTable.RawSetString("close", generatedActionFn(L, actionID, builtIn))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func ensureScopeTable(L *lua.LState, root *lua.LTable, scope string) *lua.LTable {
|
|
current := root
|
|
for segment := range strings.SplitSeq(scope, ".") {
|
|
existing := current.RawGetString(segment)
|
|
if tbl, ok := existing.(*lua.LTable); ok {
|
|
current = tbl
|
|
continue
|
|
}
|
|
next := L.NewTable()
|
|
current.RawSetString(segment, next)
|
|
current = next
|
|
}
|
|
return current
|
|
}
|
|
|
|
func generatedActionFn(L *lua.LState, canonical string, builtIn bool) *lua.LFunction {
|
|
var positionalKey string
|
|
if required := actionmeta.ActionRequiredArgs(canonical); len(required) == 1 && actionmeta.ActionArgSchema(canonical)[required[0]] == "string" {
|
|
positionalKey = required[0]
|
|
}
|
|
return L.NewFunction(func(L *lua.LState) int {
|
|
var args map[string]any
|
|
if positionalKey != "" && L.GetTop() >= 1 {
|
|
if s, ok := L.Get(1).(lua.LString); ok {
|
|
args = map[string]any{positionalKey: s.String()}
|
|
} else {
|
|
args = optionalLuaMapArg(L, 1)
|
|
}
|
|
} else {
|
|
args = optionalLuaMapArg(L, 1)
|
|
}
|
|
return yieldStep(L, step{cmd: func() tea.Msg {
|
|
return common.DispatchActionMsg{
|
|
Action: canonical,
|
|
Args: args,
|
|
BuiltIn: builtIn,
|
|
}
|
|
}})
|
|
})
|
|
}
|
|
|
|
func optionalLuaMapArg(L *lua.LState, pos int) map[string]any {
|
|
if L.GetTop() < pos || L.Get(pos) == lua.LNil {
|
|
return nil
|
|
}
|
|
tbl, ok := L.Get(pos).(*lua.LTable)
|
|
if !ok {
|
|
L.ArgError(pos, "expected table or nil")
|
|
return nil
|
|
}
|
|
return luaTableToMap(tbl)
|
|
}
|
|
|
|
func payloadFromTop(L *lua.LState) map[string]any {
|
|
if L.GetTop() >= 1 && L.CheckAny(1) != lua.LNil {
|
|
if tbl, ok := L.Get(1).(*lua.LTable); ok {
|
|
return luaTableToMap(tbl)
|
|
}
|
|
}
|
|
return map[string]any{}
|
|
}
|
|
|
|
func boolVal(payload map[string]any, key string) bool {
|
|
if v, ok := payload[key]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
return b
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func intVal(payload map[string]any, key string) int {
|
|
if v, ok := payload[key]; ok {
|
|
switch n := v.(type) {
|
|
case int:
|
|
return n
|
|
case int64:
|
|
return int(n)
|
|
case float64:
|
|
return int(n)
|
|
case float32:
|
|
return int(n)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func stringVal(payload map[string]any, key string) string {
|
|
if v, ok := payload[key]; ok {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func argsFromLua(L *lua.LState) []string {
|
|
if L.GetTop() == 0 {
|
|
return nil
|
|
}
|
|
if tbl, ok := L.Get(1).(*lua.LTable); ok {
|
|
return stringSliceFromTable(tbl)
|
|
}
|
|
var out []string
|
|
top := L.GetTop()
|
|
for i := 1; i <= top; i++ {
|
|
out = append(out, L.CheckString(i))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func stringSliceFromTable(tbl *lua.LTable) []string {
|
|
var out []string
|
|
tbl.ForEach(func(_, value lua.LValue) {
|
|
if s, ok := value.(lua.LString); ok {
|
|
out = append(out, s.String())
|
|
}
|
|
})
|
|
return out
|
|
}
|
|
|
|
func luaTableToMap(tbl *lua.LTable) map[string]any {
|
|
result := map[string]any{}
|
|
tbl.ForEach(func(key, value lua.LValue) {
|
|
if key.Type() != lua.LTString {
|
|
return
|
|
}
|
|
result[key.String()] = luaValueToGo(value)
|
|
})
|
|
return result
|
|
}
|
|
|
|
func luaTableToSlice(tbl *lua.LTable) []any {
|
|
var result []any
|
|
tbl.ForEach(func(_, value lua.LValue) {
|
|
result = append(result, luaValueToGo(value))
|
|
})
|
|
return result
|
|
}
|
|
|
|
func luaValueToGo(value lua.LValue) any {
|
|
switch value.Type() {
|
|
case lua.LTBool:
|
|
return bool(value.(lua.LBool))
|
|
case lua.LTNumber:
|
|
return float64(value.(lua.LNumber))
|
|
case lua.LTString:
|
|
return value.String()
|
|
case lua.LTTable:
|
|
t := value.(*lua.LTable)
|
|
// Heuristic: if keys are string, convert to map; otherwise, slice.
|
|
isMap := false
|
|
t.ForEach(func(key, _ lua.LValue) {
|
|
if key.Type() == lua.LTString {
|
|
isMap = true
|
|
}
|
|
})
|
|
if isMap {
|
|
return luaTableToMap(t)
|
|
}
|
|
return luaTableToSlice(t)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func yieldStep(L *lua.LState, st step) int {
|
|
ud := L.NewUserData()
|
|
ud.Value = st
|
|
return L.Yield(ud)
|
|
}
|
|
|
|
func boolPtr(v bool) *bool {
|
|
return &v
|
|
}
|
|
|
|
func parseNavigateTarget(val string) intents.NavigationTarget {
|
|
switch strings.ToLower(val) {
|
|
case "parent":
|
|
return intents.TargetParent
|
|
case "child", "children":
|
|
return intents.TargetChild
|
|
case "working", "working_copy", "work":
|
|
return intents.TargetWorkingCopy
|
|
default:
|
|
return intents.TargetNone
|
|
}
|
|
}
|
|
|
|
func matchUpdateRevisionsSuccess(msg tea.Msg) (bool, []lua.LValue) {
|
|
switch msg.(type) {
|
|
case common.UpdateRevisionsSuccessMsg, common.UpdateRevisionsFailedMsg:
|
|
return true, nil
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func matchCloseViewMsg(msg tea.Msg) (bool, []lua.LValue) {
|
|
if closeMsg, ok := msg.(common.CloseViewMsg); ok {
|
|
return true, []lua.LValue{lua.LBool(closeMsg.Applied)}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func matchChoose(msg tea.Msg) (bool, []lua.LValue) {
|
|
switch msg := msg.(type) {
|
|
case choose.SelectedMsg:
|
|
return true, []lua.LValue{lua.LString(msg.Value)}
|
|
case choose.CancelledMsg:
|
|
return true, []lua.LValue{lua.LNil}
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func matchInput(msg tea.Msg) (bool, []lua.LValue) {
|
|
switch msg := msg.(type) {
|
|
case input.SelectedMsg:
|
|
return true, []lua.LValue{lua.LString(msg.Value)}
|
|
case input.CancelledMsg:
|
|
return true, []lua.LValue{lua.LNil}
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func actionTokenFromCanonical(actionID string) string {
|
|
if idx := strings.LastIndexByte(actionID, '.'); idx >= 0 && idx < len(actionID)-1 {
|
|
return actionID[idx+1:]
|
|
}
|
|
return actionID
|
|
}
|