Compare commits

...

6 Commits

Author SHA1 Message Date
TorchedSammy 1e899bf18e
chore: set name of history in menu to History instead of file 2022-03-19 13:24:12 -04:00
TorchedSammy f03f8c0da1
docs: add exitCode to job docs 2022-03-19 13:14:12 -04:00
TorchedSammy 1378a74e87
feat: add job hooks (part of #109) 2022-03-19 13:10:50 -04:00
TorchedSammy 63bc398f1c
fix: use unexported alias handler init function 2022-03-19 12:44:26 -04:00
TorchedSammy 579a0cd0ce
refactor: rename hilbishAliases to aliasHandler for clarity 2022-03-19 12:43:48 -04:00
TorchedSammy f433ab8a6f
docs(guide): mention that users can copy the default dir from dataDir 2022-03-19 09:50:51 -04:00
7 changed files with 257 additions and 36 deletions

View File

@ -7,54 +7,54 @@ import (
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
) )
var aliases *hilbishAliases var aliases *aliasHandler
type hilbishAliases struct { type aliasHandler struct {
aliases map[string]string aliases map[string]string
mu *sync.RWMutex mu *sync.RWMutex
} }
// initialize aliases map // initialize aliases map
func NewAliases() *hilbishAliases { func newAliases() *aliasHandler {
return &hilbishAliases{ return &aliasHandler{
aliases: make(map[string]string), aliases: make(map[string]string),
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
} }
} }
func (h *hilbishAliases) Add(alias, cmd string) { func (a *aliasHandler) Add(alias, cmd string) {
h.mu.Lock() a.mu.Lock()
defer h.mu.Unlock() defer a.mu.Unlock()
h.aliases[alias] = cmd a.aliases[alias] = cmd
} }
func (h *hilbishAliases) All() map[string]string { func (a *aliasHandler) All() map[string]string {
return h.aliases return a.aliases
} }
func (h *hilbishAliases) Delete(alias string) { func (a *aliasHandler) Delete(alias string) {
h.mu.Lock() a.mu.Lock()
defer h.mu.Unlock() defer a.mu.Unlock()
delete(h.aliases, alias) delete(a.aliases, alias)
} }
func (h *hilbishAliases) Resolve(cmdstr string) string { func (a *aliasHandler) Resolve(cmdstr string) string {
h.mu.RLock() a.mu.RLock()
defer h.mu.RUnlock() defer a.mu.RUnlock()
args := strings.Split(cmdstr, " ") args := strings.Split(cmdstr, " ")
for h.aliases[args[0]] != "" { for a.aliases[args[0]] != "" {
alias := h.aliases[args[0]] alias := a.aliases[args[0]]
cmdstr = alias + strings.TrimPrefix(cmdstr, args[0]) cmdstr = alias + strings.TrimPrefix(cmdstr, args[0])
cmdArgs, _ := splitInput(cmdstr) cmdArgs, _ := splitInput(cmdstr)
args = cmdArgs args = cmdArgs
if h.aliases[args[0]] == alias { if a.aliases[args[0]] == alias {
break break
} }
if h.aliases[args[0]] != "" { if a.aliases[args[0]] != "" {
continue continue
} }
} }
@ -64,12 +64,12 @@ func (h *hilbishAliases) Resolve(cmdstr string) string {
// lua section // lua section
func (h *hilbishAliases) Loader(L *lua.LState) *lua.LTable { func (a *aliasHandler) Loader(L *lua.LState) *lua.LTable {
// create a lua module with our functions // create a lua module with our functions
hshaliasesLua := map[string]lua.LGFunction{ hshaliasesLua := map[string]lua.LGFunction{
"add": h.luaAdd, "add": a.luaAdd,
"list": h.luaList, "list": a.luaList,
"del": h.luaDelete, "del": a.luaDelete,
} }
mod := L.SetFuncs(L.NewTable(), hshaliasesLua) mod := L.SetFuncs(L.NewTable(), hshaliasesLua)
@ -77,17 +77,17 @@ func (h *hilbishAliases) Loader(L *lua.LState) *lua.LTable {
return mod return mod
} }
func (h *hilbishAliases) luaAdd(L *lua.LState) int { func (a *aliasHandler) luaAdd(L *lua.LState) int {
alias := L.CheckString(1) alias := L.CheckString(1)
cmd := L.CheckString(2) cmd := L.CheckString(2)
h.Add(alias, cmd) a.Add(alias, cmd)
return 0 return 0
} }
func (h *hilbishAliases) luaList(L *lua.LState) int { func (a *aliasHandler) luaList(L *lua.LState) int {
aliasesList := L.NewTable() aliasesList := L.NewTable()
for k, v := range h.All() { for k, v := range a.All() {
aliasesList.RawSetString(k, lua.LString(v)) aliasesList.RawSetString(k, lua.LString(v))
} }
@ -96,9 +96,9 @@ func (h *hilbishAliases) luaList(L *lua.LState) int {
return 1 return 1
} }
func (h *hilbishAliases) luaDelete(L *lua.LState) int { func (a *aliasHandler) luaDelete(L *lua.LState) int {
alias := L.CheckString(1) alias := L.CheckString(1)
h.Delete(alias) a.Delete(alias)
return 0 return 0
} }

4
api.go
View File

@ -88,7 +88,7 @@ Check out the {blue}{bold}guide{reset} command to get started.
L.SetField(mod, "os", hshos) L.SetField(mod, "os", hshos)
// hilbish.aliases table // hilbish.aliases table
aliases = NewAliases() aliases = newAliases()
aliasesModule := aliases.Loader(L) aliasesModule := aliases.Loader(L)
util.Document(L, aliasesModule, "Alias inferface for Hilbish.") util.Document(L, aliasesModule, "Alias inferface for Hilbish.")
L.SetField(mod, "aliases", aliasesModule) L.SetField(mod, "aliases", aliasesModule)
@ -106,6 +106,8 @@ Check out the {blue}{bold}guide{reset} command to get started.
util.Document(L, hshcomp, "Completions interface for Hilbish.") util.Document(L, hshcomp, "Completions interface for Hilbish.")
L.SetField(mod, "completion", hshcomp) L.SetField(mod, "completion", hshcomp)
jobs = newJobHandler()
L.Push(mod) L.Push(mod)
return 1 return 1

13
docs/hooks/job.txt 100644
View File

@ -0,0 +1,13 @@
Note: A `job` is a table with the following keys:
- cmd: command string
- running: boolean whether the job is running
- id: unique id for the job
- pid: process id for the job
- exitCode: exit code of the job
In ordinary cases you'd prefer to use the id instead of pid. The id is unique to
Hilbish and is how you get jobs with the `hilbish.jobs` interface.
+ `job.start` -> job > Thrown when a new background job starts.
+ `job.done` -> job > Thrown when a background jobs exits.

134
exec.go
View File

@ -1,13 +1,16 @@
package main package main
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"os/exec"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"syscall"
"time" "time"
"hilbish/util" "hilbish/util"
@ -17,6 +20,7 @@ import (
//"github.com/yuin/gopher-lua/parse" //"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
) )
var errNotExec = errors.New("not executable") var errNotExec = errors.New("not executable")
@ -92,6 +96,7 @@ func execCommand(cmd, old string) error {
return err return err
} }
var bg bool
exechandle := func(ctx context.Context, args []string) error { exechandle := func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " ")) _, argstring := splitInput(strings.Join(args, " "))
// i dont really like this but it works // i dont really like this but it works
@ -150,15 +155,138 @@ func execCommand(cmd, old string) error {
return interp.NewExitStatus(127) return interp.NewExitStatus(127)
} }
return interp.DefaultExecHandler(2 * time.Second)(ctx, args) killTimeout := 2 * time.Second
// from here is basically copy-paste of the default exec handler from
// sh/interp but with our job handling
hc := interp.HandlerCtx(ctx)
path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
if err != nil {
fmt.Fprintln(hc.Stderr, err)
return interp.NewExitStatus(127)
}
env := hc.Env
envList := make([]string, 0, 64)
env.Each(func(name string, vr expand.Variable) bool {
if !vr.IsSet() {
// If a variable is set globally but unset in the
// runner, we need to ensure it's not part of the final
// list. Seems like zeroing the element is enough.
// This is a linear search, but this scenario should be
// rare, and the number of variables shouldn't be large.
for i, kv := range envList {
if strings.HasPrefix(kv, name+"=") {
envList[i] = ""
}
}
}
if vr.Exported && vr.Kind == expand.String {
envList = append(envList, name+"="+vr.String())
}
return true
})
cmd := exec.Cmd{
Path: path,
Args: args,
Env: envList,
Dir: hc.Dir,
Stdin: hc.Stdin,
Stdout: hc.Stdout,
Stderr: hc.Stderr,
}
err = cmd.Start()
job := jobs.getLatest()
if err == nil {
if bg {
job.start(cmd.Process.Pid)
}
if done := ctx.Done(); done != nil {
go func() {
<-done
if killTimeout <= 0 || runtime.GOOS == "windows" {
cmd.Process.Signal(os.Kill)
return
}
// TODO: don't temporarily leak this goroutine
// if the program stops itself with the
// interrupt.
go func() {
time.Sleep(killTimeout)
cmd.Process.Signal(os.Kill)
}()
cmd.Process.Signal(os.Interrupt)
}()
}
err = cmd.Wait()
}
var exit uint8
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return ctx.Err()
}
exit = uint8(128 + status.Signal())
goto end
}
exit = uint8(status.ExitStatus())
goto end
}
exit = 1
goto end
case *exec.Error:
// did not start
fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
goto end
case nil:
goto end
default:
return err
}
end:
if bg {
job.exitCode = int(exit)
job.finish()
}
return interp.NewExitStatus(exit)
} }
runner, _ := interp.New( runner, _ := interp.New(
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(exechandle), interp.ExecHandler(exechandle),
) )
err = runner.Run(context.TODO(), file)
return err buf := new(bytes.Buffer)
printer := syntax.NewPrinter()
for _, stmt := range file.Stmts {
bg = false
if stmt.Background {
bg = true
printer.Print(buf, stmt.Cmd)
stmtStr := buf.String()
buf.Reset()
jobs.add(stmtStr)
}
err = runner.Run(context.TODO(), stmt)
if err != nil {
return err
}
}
return nil
} }
func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable

75
job.go 100644
View File

@ -0,0 +1,75 @@
package main
import (
"sync"
"github.com/yuin/gopher-lua"
)
var jobs *jobHandler
type job struct {
cmd string
running bool
id int
pid int
exitCode int
}
func (j *job) start(pid int) {
j.pid = pid
j.running = true
hooks.Em.Emit("job.start", j.lua())
}
func (j *job) finish() {
j.running = false
hooks.Em.Emit("job.done", j.lua())
}
func (j *job) lua() *lua.LTable {
// returns lua table for job
// because userdata is gross
luaJob := l.NewTable()
l.SetField(luaJob, "cmd", lua.LString(j.cmd))
l.SetField(luaJob, "running", lua.LBool(j.running))
l.SetField(luaJob, "id", lua.LNumber(j.id))
l.SetField(luaJob, "pid", lua.LNumber(j.pid))
l.SetField(luaJob, "exitCode", lua.LNumber(j.exitCode))
return luaJob
}
type jobHandler struct {
jobs map[int]*job
latestID int
mu *sync.RWMutex
}
func newJobHandler() *jobHandler {
return &jobHandler{
jobs: make(map[int]*job),
latestID: 0,
mu: &sync.RWMutex{},
}
}
func (j *jobHandler) add(cmd string) {
j.mu.Lock()
defer j.mu.Unlock()
j.latestID++
j.jobs[j.latestID] = &job{
cmd: cmd,
running: false,
id: j.latestID,
}
}
func (j *jobHandler) getLatest() *job {
j.mu.RLock()
defer j.mu.RUnlock()
return j.jobs[j.latestID]
}

View File

@ -168,6 +168,9 @@ hilbish.userDir.config .. '/hilbish/init.lua' ..
and also change all global functions (prompt, alias) to be and also change all global functions (prompt, alias) to be
in the hilbish module (hilbish.prompt, hilbish.alias as examples). in the hilbish module (hilbish.prompt, hilbish.alias as examples).
And if this is your first time (most likely), you can copy a config
from ]] .. hilbish.dataDir,
[[
Since 1.0 is a big release, you'll want to check the changelog Since 1.0 is a big release, you'll want to check the changelog
at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0 at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0
to find more breaking changes. to find more breaking changes.

2
rl.go
View File

@ -21,7 +21,7 @@ func newLineReader(prompt string, noHist bool) *lineReader {
// but it cant have shared history // but it cant have shared history
if !noHist { if !noHist {
fileHist = newFileHistory() fileHist = newFileHistory()
rl.SetHistoryCtrlR("file", fileHist) rl.SetHistoryCtrlR("History", fileHist)
rl.HistoryAutoWrite = false rl.HistoryAutoWrite = false
} }
rl.ShowVimMode = false rl.ShowVimMode = false