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"
)
var aliases *hilbishAliases
var aliases *aliasHandler
type hilbishAliases struct {
type aliasHandler struct {
aliases map[string]string
mu *sync.RWMutex
}
// initialize aliases map
func NewAliases() *hilbishAliases {
return &hilbishAliases{
func newAliases() *aliasHandler {
return &aliasHandler{
aliases: make(map[string]string),
mu: &sync.RWMutex{},
}
}
func (h *hilbishAliases) Add(alias, cmd string) {
h.mu.Lock()
defer h.mu.Unlock()
func (a *aliasHandler) Add(alias, cmd string) {
a.mu.Lock()
defer a.mu.Unlock()
h.aliases[alias] = cmd
a.aliases[alias] = cmd
}
func (h *hilbishAliases) All() map[string]string {
return h.aliases
func (a *aliasHandler) All() map[string]string {
return a.aliases
}
func (h *hilbishAliases) Delete(alias string) {
h.mu.Lock()
defer h.mu.Unlock()
func (a *aliasHandler) Delete(alias string) {
a.mu.Lock()
defer a.mu.Unlock()
delete(h.aliases, alias)
delete(a.aliases, alias)
}
func (h *hilbishAliases) Resolve(cmdstr string) string {
h.mu.RLock()
defer h.mu.RUnlock()
func (a *aliasHandler) Resolve(cmdstr string) string {
a.mu.RLock()
defer a.mu.RUnlock()
args := strings.Split(cmdstr, " ")
for h.aliases[args[0]] != "" {
alias := h.aliases[args[0]]
for a.aliases[args[0]] != "" {
alias := a.aliases[args[0]]
cmdstr = alias + strings.TrimPrefix(cmdstr, args[0])
cmdArgs, _ := splitInput(cmdstr)
args = cmdArgs
if h.aliases[args[0]] == alias {
if a.aliases[args[0]] == alias {
break
}
if h.aliases[args[0]] != "" {
if a.aliases[args[0]] != "" {
continue
}
}
@ -64,12 +64,12 @@ func (h *hilbishAliases) Resolve(cmdstr string) string {
// 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
hshaliasesLua := map[string]lua.LGFunction{
"add": h.luaAdd,
"list": h.luaList,
"del": h.luaDelete,
"add": a.luaAdd,
"list": a.luaList,
"del": a.luaDelete,
}
mod := L.SetFuncs(L.NewTable(), hshaliasesLua)
@ -77,17 +77,17 @@ func (h *hilbishAliases) Loader(L *lua.LState) *lua.LTable {
return mod
}
func (h *hilbishAliases) luaAdd(L *lua.LState) int {
func (a *aliasHandler) luaAdd(L *lua.LState) int {
alias := L.CheckString(1)
cmd := L.CheckString(2)
h.Add(alias, cmd)
a.Add(alias, cmd)
return 0
}
func (h *hilbishAliases) luaList(L *lua.LState) int {
func (a *aliasHandler) luaList(L *lua.LState) int {
aliasesList := L.NewTable()
for k, v := range h.All() {
for k, v := range a.All() {
aliasesList.RawSetString(k, lua.LString(v))
}
@ -96,9 +96,9 @@ func (h *hilbishAliases) luaList(L *lua.LState) int {
return 1
}
func (h *hilbishAliases) luaDelete(L *lua.LState) int {
func (a *aliasHandler) luaDelete(L *lua.LState) int {
alias := L.CheckString(1)
h.Delete(alias)
a.Delete(alias)
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)
// hilbish.aliases table
aliases = NewAliases()
aliases = newAliases()
aliasesModule := aliases.Loader(L)
util.Document(L, aliasesModule, "Alias inferface for Hilbish.")
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.")
L.SetField(mod, "completion", hshcomp)
jobs = newJobHandler()
L.Push(mod)
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.

132
exec.go
View File

@ -1,13 +1,16 @@
package main
import (
"bytes"
"context"
"errors"
"os/exec"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"hilbish/util"
@ -17,6 +20,7 @@ import (
//"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
)
var errNotExec = errors.New("not executable")
@ -92,6 +96,7 @@ func execCommand(cmd, old string) error {
return err
}
var bg bool
exechandle := func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " "))
// i dont really like this but it works
@ -150,16 +155,139 @@ func execCommand(cmd, old string) error {
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(
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(exechandle),
)
err = runner.Run(context.TODO(), file)
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
var skip []string

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
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
at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0
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
if !noHist {
fileHist = newFileHistory()
rl.SetHistoryCtrlR("file", fileHist)
rl.SetHistoryCtrlR("History", fileHist)
rl.HistoryAutoWrite = false
}
rl.ShowVimMode = false