diff --git a/cmd/micro/initlua.go b/cmd/micro/initlua.go index 269fbe9d..804f1142 100644 --- a/cmd/micro/initlua.go +++ b/cmd/micro/initlua.go @@ -7,6 +7,7 @@ import ( luar "layeh.com/gopher-luar" "github.com/zyedidia/micro/internal/action" + "github.com/zyedidia/micro/internal/buffer" "github.com/zyedidia/micro/internal/display" ulua "github.com/zyedidia/micro/internal/lua" "github.com/zyedidia/micro/internal/screen" @@ -25,6 +26,8 @@ func LuaImport(pkg string) *lua.LTable { return luaImportMicro() case "micro/shell": return luaImportMicroShell() + case "micro/buffer": + return luaImportMicroBuffer() case "micro/util": return luaImportMicroUtil() default: @@ -52,6 +55,22 @@ func luaImportMicroShell() *lua.LTable { ulua.L.SetField(pkg, "RunCommand", luar.New(ulua.L, shell.RunCommand)) ulua.L.SetField(pkg, "RunBackgroundShell", luar.New(ulua.L, shell.RunBackgroundShell)) ulua.L.SetField(pkg, "RunInteractiveShell", luar.New(ulua.L, shell.RunInteractiveShell)) + ulua.L.SetField(pkg, "JobStart", luar.New(ulua.L, shell.JobStart)) + ulua.L.SetField(pkg, "JobSpawn", luar.New(ulua.L, shell.JobSpawn)) + ulua.L.SetField(pkg, "JobStop", luar.New(ulua.L, shell.JobStop)) + ulua.L.SetField(pkg, "JobSend", luar.New(ulua.L, shell.JobSend)) + + return pkg +} + +func luaImportMicroBuffer() *lua.LTable { + pkg := ulua.L.NewTable() + + ulua.L.SetField(pkg, "NewMessage", luar.New(ulua.L, buffer.NewMessage)) + ulua.L.SetField(pkg, "NewMessageAtLine", luar.New(ulua.L, buffer.NewMessageAtLine)) + ulua.L.SetField(pkg, "MTInfo", luar.New(ulua.L, buffer.MTInfo)) + ulua.L.SetField(pkg, "MTWarning", luar.New(ulua.L, buffer.MTWarning)) + ulua.L.SetField(pkg, "MTError", luar.New(ulua.L, buffer.MTError)) return pkg } @@ -61,7 +80,7 @@ func luaImportMicroUtil() *lua.LTable { ulua.L.SetField(pkg, "RuneAt", luar.New(ulua.L, util.LuaRuneAt)) ulua.L.SetField(pkg, "GetLeadingWhitespace", luar.New(ulua.L, util.LuaGetLeadingWhitespace)) - ulua.L.SetField(pkg, "IsWordChar", luar.New(ulua.L, util.LuaIsWordChar)) + ulua.L.SetField(pkg, "", luar.New(ulua.L, util.LuaIsWordChar)) return pkg } diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 52f69518..06e1f968 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -13,6 +13,7 @@ import ( "github.com/zyedidia/micro/internal/buffer" "github.com/zyedidia/micro/internal/config" "github.com/zyedidia/micro/internal/screen" + "github.com/zyedidia/micro/internal/shell" "github.com/zyedidia/micro/internal/util" "github.com/zyedidia/tcell" ) @@ -231,6 +232,9 @@ func main() { // Check for new events select { + case f := <-shell.Jobs: + // If a new job has finished while running in the background we should execute the callback + f.Function(f.Output, f.Args...) case event = <-events: case <-screen.DrawChan: } diff --git a/internal/config/plugin.go b/internal/config/plugin.go index 8b9d93e3..8023c266 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -114,3 +114,14 @@ func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) { ulua.L.Pop(1) return ret, nil } + +func FindPlugin(name string) *Plugin { + var pl *Plugin + for _, p := range Plugins { + if p.Name == name { + pl = p + break + } + } + return pl +} diff --git a/internal/display/statusline.go b/internal/display/statusline.go index e4a6a1de..059c25d2 100644 --- a/internal/display/statusline.go +++ b/internal/display/statusline.go @@ -54,13 +54,7 @@ var statusInfo = map[string]func(*buffer.Buffer) string{ func SetStatusInfoFnLua(s string, fn string) { luaFn := strings.Split(fn, ".") plName, plFn := luaFn[0], luaFn[1] - var pl *config.Plugin - for _, p := range config.Plugins { - if p.Name == plName { - pl = p - break - } - } + pl := config.FindPlugin(plName) statusInfo[s] = func(b *buffer.Buffer) string { if pl == nil || !pl.IsEnabled() { return "" diff --git a/internal/shell/job.go b/internal/shell/job.go new file mode 100644 index 00000000..d3c33bf8 --- /dev/null +++ b/internal/shell/job.go @@ -0,0 +1,129 @@ +package shell + +import ( + "bytes" + "io" + "os/exec" + "strings" + + luar "layeh.com/gopher-luar" + + lua "github.com/yuin/gopher-lua" + "github.com/zyedidia/micro/internal/config" + ulua "github.com/zyedidia/micro/internal/lua" + "github.com/zyedidia/micro/internal/screen" +) + +var Jobs chan JobFunction + +func init() { + Jobs = make(chan JobFunction, 100) +} + +// Jobs are the way plugins can run processes in the background +// A job is simply a process that gets executed asynchronously +// There are callbacks for when the job exits, when the job creates stdout +// and when the job creates stderr + +// These jobs run in a separate goroutine but the lua callbacks need to be +// executed in the main thread (where the Lua VM is running) so they are +// put into the jobs channel which gets read by the main loop + +// JobFunction is a representation of a job (this data structure is what is loaded +// into the jobs channel) +type JobFunction struct { + Function func(string, ...string) + Output string + Args []string +} + +// A CallbackFile is the data structure that makes it possible to catch stderr and stdout write events +type CallbackFile struct { + io.Writer + + callback func(string, ...string) + args []string +} + +func (f *CallbackFile) Write(data []byte) (int, error) { + // This is either stderr or stdout + // In either case we create a new job function callback and put it in the jobs channel + jobFunc := JobFunction{f.callback, string(data), f.args} + Jobs <- jobFunc + return f.Writer.Write(data) +} + +// JobStart starts a shell command in the background with the given callbacks +// It returns an *exec.Cmd as the job id +func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd { + return JobSpawn("sh", []string{"-c", cmd}, onStdout, onStderr, onExit, userargs...) +} + +// JobSpawn starts a process with args in the background with the given callbacks +// It returns an *exec.Cmd as the job id +func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd { + // Set up everything correctly if the functions have been provided + proc := exec.Command(cmdName, cmdArgs...) + var outbuf bytes.Buffer + if onStdout != "" { + proc.Stdout = &CallbackFile{&outbuf, luaFunctionJob(onStdout), userargs} + } else { + proc.Stdout = &outbuf + } + if onStderr != "" { + proc.Stderr = &CallbackFile{&outbuf, luaFunctionJob(onStderr), userargs} + } else { + proc.Stderr = &outbuf + } + + go func() { + // Run the process in the background and create the onExit callback + proc.Run() + jobFunc := JobFunction{luaFunctionJob(onExit), string(outbuf.Bytes()), userargs} + Jobs <- jobFunc + }() + + return proc +} + +// JobStop kills a job +func JobStop(cmd *exec.Cmd) { + cmd.Process.Kill() +} + +// JobSend sends the given data into the job's stdin stream +func JobSend(cmd *exec.Cmd, data string) { + stdin, err := cmd.StdinPipe() + if err != nil { + return + } + + stdin.Write([]byte(data)) +} + +// luaFunctionJob returns a function that will call the given lua function +// structured as a job call i.e. the job output and arguments are provided +// to the lua function +func luaFunctionJob(fn string) func(string, ...string) { + luaFn := strings.Split(fn, ".") + plName, plFn := luaFn[0], luaFn[1] + pl := config.FindPlugin(plName) + return func(output string, args ...string) { + var luaArgs []lua.LValue + for _, v := range args { + luaArgs = append(luaArgs, luar.New(ulua.L, v)) + } + _, err := pl.Call(plFn, luaArgs...) + if err != nil && err != config.ErrNoSuchFunction { + screen.TermMessage(err) + } + } +} + +func unpack(old []string) []interface{} { + new := make([]interface{}, len(old)) + for i, v := range old { + new[i] = v + } + return new +}