From 0b0821f410ebc9eda9c71a052a570ea8c55b3185 Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sat, 28 Dec 2024 21:43:33 -0400
Subject: [PATCH 01/14] refactor: make initial changes for snail lib (shell
 interp)

---
 api.go                                      |  4 +-
 exec.go                                     | 45 +--------------------
 golibs/fs/fs.go                             |  2 -
 golibs/terminal/snail.go                    |  0
 job.go                                      |  4 +-
 job_unix.go                                 |  4 ++
 job_windows.go                              |  5 +++
 main.go                                     |  2 -
 nature/commands/cd.lua                      |  8 ++--
 nature/dirs.lua                             |  6 +++
 util/util.go                                | 25 ++++++++++++
 execfile_unix.go => util/util_unix.go       |  9 +----
 execfile_windows.go => util/util_windows.go |  9 +----
 13 files changed, 52 insertions(+), 71 deletions(-)
 create mode 100644 golibs/terminal/snail.go
 rename execfile_unix.go => util/util_unix.go (61%)
 rename execfile_windows.go => util/util_windows.go (74%)

diff --git a/api.go b/api.go
index 43e361a..8c2e6e4 100644
--- a/api.go
+++ b/api.go
@@ -508,7 +508,7 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	}
 	cmdArgs, _ := splitInput(cmd)
 	if runtime.GOOS != "windows" {
-		cmdPath, err := exec.LookPath(cmdArgs[0])
+		cmdPath, err := util.LookPath(cmdArgs[0])
 		if err != nil {
 			fmt.Println(err)
 			// if we get here, cmdPath will be nothing
@@ -706,7 +706,7 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil
 	}
 
-	path, err := exec.LookPath(cmd)
+	path, err := util.LookPath(cmd)
 	if err != nil {
 		return c.Next(), nil
 	}
diff --git a/exec.go b/exec.go
index 7f8e37b..01a5dfa 100644
--- a/exec.go
+++ b/exec.go
@@ -411,7 +411,7 @@ func execHandle(bg bool) interp.ExecHandlerFunc {
 			return interp.NewExitStatus(exitcode)
 		}
 
-		path, err := lookpath(args[0])
+		path, err := util.LookPath(args[0])
 		if err == errNotExec {
 			return execError{
 				typ: "not-executable",
@@ -524,41 +524,14 @@ func handleExecErr(err error) (exit uint8) {
 
 	return
 }
-func lookpath(file string) (string, error) { // custom lookpath function so we know if a command is found *and* is executable
-	var skip []string
-	if runtime.GOOS == "windows" {
-		skip = []string{"./", "../", "~/", "C:"}
-	} else {
-		skip = []string{"./", "/", "../", "~/"}
-	}
-	for _, s := range skip {
-		if strings.HasPrefix(file, s) {
-			return file, findExecutable(file, false, false)
-		}
-	}
-	for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
-		path := filepath.Join(dir, file)
-		err := findExecutable(path, true, false)
-		if err == errNotExec {
-			return "", err
-		} else if err == nil {
-			return path, nil
-		}
-	}
-
-	return "", os.ErrNotExist
-}
 
 func splitInput(input string) ([]string, string) {
 	// end my suffering
 	// TODO: refactor this garbage
 	quoted := false
-	startlastcmd := false
-	lastcmddone := false
 	cmdArgs := []string{}
 	sb := &strings.Builder{}
 	cmdstr := &strings.Builder{}
-	lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1)
 
 	for _, r := range input {
 		if r == '"' {
@@ -574,22 +547,6 @@ func splitInput(input string) ([]string, string) {
 			// if not quoted and there's a space then add to cmdargs
 			cmdArgs = append(cmdArgs, sb.String())
 			sb.Reset()
-		} else if !quoted && r == '^' && startlastcmd && !lastcmddone {
-			// if ^ is found, isnt in quotes and is
-			// the second occurence of the character and is
-			// the first time "^^" has been used
-			cmdstr.WriteString(lastcmd)
-			sb.WriteString(lastcmd)
-
-			startlastcmd = !startlastcmd
-			lastcmddone = !lastcmddone
-
-			continue
-		} else if !quoted && r == '^' && !lastcmddone {
-			// if ^ is found, isnt in quotes and is the
-			// first time of starting "^^"
-			startlastcmd = !startlastcmd
-			continue
 		} else {
 			sb.WriteRune(r)
 		}
diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go
index 002be90..1c0589e 100644
--- a/golibs/fs/fs.go
+++ b/golibs/fs/fs.go
@@ -110,12 +110,10 @@ func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	}
 	path = util.ExpandHome(strings.TrimSpace(path))
 
-	abspath, _ := filepath.Abs(path)
 	err = os.Chdir(path)
 	if err != nil {
 		return nil, err
 	}
-	interp.Dir(abspath)(f.runner)
 
 	return c.Next(), err
 }
diff --git a/golibs/terminal/snail.go b/golibs/terminal/snail.go
new file mode 100644
index 0000000..e69de29
diff --git a/job.go b/job.go
index f5bd6f2..bb16e92 100644
--- a/job.go
+++ b/job.go
@@ -56,8 +56,8 @@ func (j *job) start() error {
 		}
 		j.setHandle(&cmd)
 	}
-	// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct
-	// in a simple explanation, it makes signals from hilbish (sigint)
+	// bgProcAttr is defined in job_<os>.go, it holds a procattr struct
+	// in a simple explanation, it makes signals from hilbish (like sigint)
 	// not go to it (child process)
 	j.handle.SysProcAttr = bgProcAttr
 	// reset output buffers
diff --git a/job_unix.go b/job_unix.go
index 0a038b1..2caa4ae 100644
--- a/job_unix.go
+++ b/job_unix.go
@@ -10,6 +10,10 @@ import (
 	"golang.org/x/sys/unix"
 )
 
+var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
+	Setpgid: true,
+}
+
 func (j *job) foreground() error {
 	if jobs.foreground {
 		return errors.New("(another) job already foregrounded")
diff --git a/job_windows.go b/job_windows.go
index 26818b5..1ac4646 100644
--- a/job_windows.go
+++ b/job_windows.go
@@ -4,8 +4,13 @@ package main
 
 import (
 	"errors"
+	"syscall"
 )
 
+var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
+	CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
+}
+
 func (j *job) foreground() error {
 	return errors.New("not supported on windows")
 }
diff --git a/main.go b/main.go
index 1bddfc4..af7a22a 100644
--- a/main.go
+++ b/main.go
@@ -38,11 +38,9 @@ var (
 	cmds *commander.Commander
 	defaultConfPath string
 	defaultHistPath string
-	runner *interp.Runner
 )
 
 func main() {
-	runner, _ = interp.New()
 	curuser, _ = user.Current()
 	homedir := curuser.HomeDir
 	confDir, _ = os.UserConfigDir()
diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua
index 7cfe4a2..0efbb3f 100644
--- a/nature/commands/cd.lua
+++ b/nature/commands/cd.lua
@@ -3,8 +3,9 @@ local commander = require 'commander'
 local fs = require 'fs'
 local dirs = require 'nature.dirs'
 
-dirs.old = hilbish.cwd()
 commander.register('cd', function (args, sinks)
+	local oldPath = hilbish.cwd()
+
 	if #args > 1 then
 		sinks.out:writeln("cd: too many arguments")
 		return 1
@@ -16,13 +17,10 @@ commander.register('cd', function (args, sinks)
 		sinks.out:writeln(path)
 	end
 
-	dirs.setOld(hilbish.cwd())
-	dirs.push(path)
-
 	local ok, err = pcall(function() fs.cd(path) end)
 	if not ok then
 		sinks.out:writeln(err)
 		return 1
 	end
-	bait.throw('cd', path)
+	bait.throw('hilbish.cd', fs.abs(path), oldPath)
 end)
diff --git a/nature/dirs.lua b/nature/dirs.lua
index 328b4b7..2efad63 100644
--- a/nature/dirs.lua
+++ b/nature/dirs.lua
@@ -1,4 +1,5 @@
 -- @module dirs
+local bait = require 'bait'
 local fs = require 'fs'
 
 local dirs = {}
@@ -73,4 +74,9 @@ function dirs.setOld(d)
 	dirs.old = d
 end
 
+bait.catch('hilbish.cd', function(path, oldPath)
+	dirs.setOld(oldPath)
+	dirs.push(path)
+end)
+
 return dirs
diff --git a/util/util.go b/util/util.go
index 0fcd4b0..e60f66e 100644
--- a/util/util.go
+++ b/util/util.go
@@ -141,3 +141,28 @@ func AbbrevHome(path string) string {
 
 	return path
 }
+
+func LookPath(file string) (string, error) { // custom lookpath function so we know if a command is found *and* is executable
+	var skip []string
+	if runtime.GOOS == "windows" {
+		skip = []string{"./", "../", "~/", "C:"}
+	} else {
+		skip = []string{"./", "/", "../", "~/"}
+	}
+	for _, s := range skip {
+		if strings.HasPrefix(file, s) {
+			return file, findExecutable(file, false, false)
+		}
+	}
+	for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
+		path := filepath.Join(dir, file)
+		err := findExecutable(path, true, false)
+		if err == errNotExec {
+			return "", err
+		} else if err == nil {
+			return path, nil
+		}
+	}
+
+	return "", os.ErrNotExist
+}
diff --git a/execfile_unix.go b/util/util_unix.go
similarity index 61%
rename from execfile_unix.go
rename to util/util_unix.go
index 82c738b..9fa6a6c 100644
--- a/execfile_unix.go
+++ b/util/util_unix.go
@@ -1,17 +1,12 @@
 //go:build unix
 
-package main
+package util
 
 import (
 	"os"
-	"syscall"
 )
 
-var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
-	Setpgid: true,
-}
-
-func findExecutable(path string, inPath, dirs bool) error {
+func FindExecutable(path string, inPath, dirs bool) error {
 	f, err := os.Stat(path)
 	if err != nil {
 		return err
diff --git a/execfile_windows.go b/util/util_windows.go
similarity index 74%
rename from execfile_windows.go
rename to util/util_windows.go
index 3d6ef61..4eb50c8 100644
--- a/execfile_windows.go
+++ b/util/util_windows.go
@@ -1,18 +1,13 @@
 //go:build windows
 
-package main
+package util
 
 import (
 	"path/filepath"
 	"os"
-	"syscall"
 )
 
-var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
-	CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
-}
-
-func findExecutable(path string, inPath, dirs bool) error {
+func FindExecutable(path string, inPath, dirs bool) error {
 	nameExt := filepath.Ext(path)
 	pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
 	if inPath {

From 571fcc3e9ee5f40a5648a9b4d00dad2f863ac85a Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sat, 28 Dec 2024 22:59:41 -0400
Subject: [PATCH 02/14] fix: remove sh runner dep from fs

---
 golibs/fs/fs.go | 61 +++++++++++++++++++------------------------------
 lua.go          |  3 +--
 2 files changed, 25 insertions(+), 39 deletions(-)

diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go
index 1c0589e..9e03325 100644
--- a/golibs/fs/fs.go
+++ b/golibs/fs/fs.go
@@ -19,38 +19,25 @@ import (
 	rt "github.com/arnodel/golua/runtime"
 	"github.com/arnodel/golua/lib/packagelib"
 	"github.com/arnodel/golua/lib/iolib"
-	"mvdan.cc/sh/v3/interp"
 )
 
-type fs struct{
-	runner *interp.Runner
-	Loader packagelib.Loader
+var Loader = packagelib.Loader{
+	Load: loaderFunc,
+	Name: "fs",
 }
 
-func New(runner *interp.Runner) *fs {
-	f := &fs{
-		runner: runner,
-	}
-	f.Loader = packagelib.Loader{
-		Load: f.loaderFunc,
-		Name: "fs",
-	}
-
-	return f
-}
-
-func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
+func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
 	exports := map[string]util.LuaExport{
-		"cd": util.LuaExport{f.fcd, 1, false},
-		"mkdir": util.LuaExport{f.fmkdir, 2, false},
-		"stat": util.LuaExport{f.fstat, 1, false},
-		"readdir": util.LuaExport{f.freaddir, 1, false},
-		"abs": util.LuaExport{f.fabs, 1, false},
-		"basename": util.LuaExport{f.fbasename, 1, false},
-		"dir": util.LuaExport{f.fdir, 1, false},
-		"glob": util.LuaExport{f.fglob, 1, false},
-		"join": util.LuaExport{f.fjoin, 0, true},
-		"pipe": util.LuaExport{f.fpipe, 0, false},
+		"cd": util.LuaExport{fcd, 1, false},
+		"mkdir": util.LuaExport{fmkdir, 2, false},
+		"stat": util.LuaExport{fstat, 1, false},
+		"readdir": util.LuaExport{freaddir, 1, false},
+		"abs": util.LuaExport{fabs, 1, false},
+		"basename": util.LuaExport{fbasename, 1, false},
+		"dir": util.LuaExport{fdir, 1, false},
+		"glob": util.LuaExport{fglob, 1, false},
+		"join": util.LuaExport{fjoin, 0, true},
+		"pipe": util.LuaExport{fpipe, 0, false},
 	}
 	mod := rt.NewTable()
 	util.SetExports(rtm, mod, exports)
@@ -65,7 +52,7 @@ func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
 // This can be used to resolve short paths like `..` to `/home/user`.
 // #param path string
 // #returns string
-func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	path, err := c.StringArg(0)
 	if err != nil {
 		return nil, err
@@ -85,7 +72,7 @@ func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 // `.` will be returned.
 // #param path string Path to get the base name of.
 // #returns string
-func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
 	}
@@ -100,7 +87,7 @@ func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 // cd(dir)
 // Changes Hilbish's directory to `dir`.
 // #param dir string Path to change directory to.
-func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
 	}
@@ -123,7 +110,7 @@ func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 // `~/Documents/doc.txt` then this function will return `~/Documents`.
 // #param path string Path to get the directory for.
 // #returns string
-func (f *fs) fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
 	}
@@ -154,7 +141,7 @@ print(matches)
 -- -> {'init.lua', 'code.lua'}
 #example
 */
-func (f *fs) fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
 	}
@@ -188,7 +175,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish'))
 -- -> '/home/user/.config/hilbish' on Linux
 #example
 */
-func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	strs := make([]string, len(c.Etc()))
 	for i, v := range c.Etc() {
 		if v.Type() != rt.StringType {
@@ -215,7 +202,7 @@ func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 fs.mkdir('./foo/bar', true)
 #example
 */
-func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.CheckNArgs(2); err != nil {
 		return nil, err
 	}
@@ -246,7 +233,7 @@ func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 // The type returned is a Lua file, same as returned from `io` functions.
 // #returns File
 // #returns File
-func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	rf, wf, err := os.Pipe()
 	if err != nil {
 		return nil, err
@@ -261,7 +248,7 @@ func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 // Returns a list of all files and directories in the provided path.
 // #param dir string
 // #returns table
-func (f *fs) freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
 	}
@@ -309,7 +296,7 @@ Would print the following:
 ]]--
 #example
 */
-func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
 	}
diff --git a/lua.go b/lua.go
index 88fedf8..94b7910 100644
--- a/lua.go
+++ b/lua.go
@@ -30,8 +30,7 @@ func luaInit() {
 	util.DoString(l, "hilbish = require 'hilbish'")
 
 	// Add fs and terminal module module to Lua
-	f := fs.New(runner)
-	lib.LoadLibs(l, f.Loader)
+	lib.LoadLibs(l, fs.Loader)
 	lib.LoadLibs(l, terminal.Loader)
 
 	cmds = commander.New(l)

From 52618191494ae7eeb3c373b5ed09b6f210e3f2cd Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:12:06 -0400
Subject: [PATCH 03/14] feat: implement snail library

---
 api.go                   |  16 +-
 complete.go              |   4 +-
 exec.go                  | 386 ++-------------------------------------
 golibs/snail/lua.go      | 125 +++++++++++++
 golibs/snail/snail.go    | 302 ++++++++++++++++++++++++++++++
 golibs/terminal/snail.go |   0
 job.go                   |   2 +-
 lua.go                   |   6 +-
 main.go                  |  10 -
 nature/runner.lua        |   6 +-
 runnermode.go            |   8 +-
 sink.go => sink/sink.go  |  34 ++--
 util/streams.go          |  11 ++
 util/util.go             | 118 +++++++++++-
 util/util_unix.go        |   2 +-
 15 files changed, 614 insertions(+), 416 deletions(-)
 create mode 100644 golibs/snail/lua.go
 create mode 100644 golibs/snail/snail.go
 delete mode 100644 golibs/terminal/snail.go
 rename sink.go => sink/sink.go (87%)
 create mode 100644 util/streams.go

diff --git a/api.go b/api.go
index 8c2e6e4..d556589 100644
--- a/api.go
+++ b/api.go
@@ -13,10 +13,9 @@
 package main
 
 import (
-	"bytes"
+	//"bytes"
 	"errors"
 	"fmt"
-	"io"
 	"os"
 	"os/exec"
 	"runtime"
@@ -28,9 +27,9 @@ import (
 
 	rt "github.com/arnodel/golua/runtime"
 	"github.com/arnodel/golua/lib/packagelib"
-	"github.com/arnodel/golua/lib/iolib"
+	//"github.com/arnodel/golua/lib/iolib"
 	"github.com/maxlandon/readline"
-	"mvdan.cc/sh/v3/interp"
+	//"mvdan.cc/sh/v3/interp"
 )
 
 var exports = map[string]util.LuaExport{
@@ -49,7 +48,7 @@ var exports = map[string]util.LuaExport{
 	"inputMode": {hlinputMode, 1, false},
 	"interval": {hlinterval, 2, false},
 	"read": {hlread, 1, false},
-	"run": {hlrun, 1, true},
+	//"run": {hlrun, 1, true},
 	"timeout": {hltimeout, 2, false},
 	"which": {hlwhich, 1, false},
 }
@@ -154,6 +153,7 @@ func unsetVimMode() {
 	util.SetField(l, hshMod, "vimMode", rt.NilValue)
 }
 
+/*
 func handleStream(v rt.Value, strms *streams, errStream bool) error {
 	ud, ok := v.TryUserData()
 	if !ok {
@@ -182,6 +182,7 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error {
 
 	return nil
 }
+*/
 
 // run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
 // Runs `cmd` in Hilbish's shell script interpreter.
@@ -210,6 +211,7 @@ hilbish.run('wc -l', {
 })
 */
 // #example
+/*
 func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	// TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN.
 	if err := c.Check1Arg(); err != nil {
@@ -288,6 +290,7 @@ func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 
 	return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil
 }
+*/
 
 // cwd() -> string
 // Returns the current directory of the shell.
@@ -743,6 +746,8 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 }
 
 // runnerMode(mode)
+// **NOTE: This function is deprecated and will be removed in 3.0**
+// Use `hilbish.runner.setCurrent` instead.
 // Sets the execution/runner mode for interactive Hilbish.
 // This determines whether Hilbish wll try to run input as Lua
 // and/or sh or only do one of either.
@@ -752,6 +757,7 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 // Read [about runner mode](../features/runner-mode) for more information.
 // #param mode string|function
 func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+	// TODO: Reimplement in Lua
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
 	}
diff --git a/complete.go b/complete.go
index 86938cb..e2f0812 100644
--- a/complete.go
+++ b/complete.go
@@ -98,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
 			if len(fileCompletions) != 0 {
 				for _, f := range fileCompletions {
 					fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
-					if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
+					if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
 						continue
 					}
 					completions = append(completions, f)
@@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
 			// get basename from matches
 			for _, match := range matches {
 				// check if we have execute permissions for our match
-				err := findExecutable(match, true, false)
+				err := util.FindExecutable(match, true, false)
 				if err != nil {
 					continue
 				}
diff --git a/exec.go b/exec.go
index 01a5dfa..19a5d2f 100644
--- a/exec.go
+++ b/exec.go
@@ -1,141 +1,45 @@
 package main
 
 import (
-	"bytes"
-	"context"
 	"errors"
-	"os/exec"
 	"fmt"
 	"io"
 	"os"
-	"os/signal"
-	"path/filepath"
-	"runtime"
 	"strings"
-	"syscall"
-	"time"
 
 	"hilbish/util"
+	//herror "hilbish/errors"
 
 	rt "github.com/arnodel/golua/runtime"
-	"mvdan.cc/sh/v3/shell"
 	//"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")
 var errNotFound = errors.New("not found")
 var runnerMode rt.Value = rt.StringValue("hybrid")
 
-type streams struct {
-	stdout io.Writer
-	stderr io.Writer
-	stdin io.Reader
-}
-
-type execError struct{
-	typ string
-	cmd string
-	code int
-	colon bool
-	err error
-}
-
-func (e execError) Error() string {
-	return fmt.Sprintf("%s: %s", e.cmd, e.typ)
-}
-
-func (e execError) sprint() error {
-	sep := " "
-	if e.colon {
-		sep = ": "
-	}
-
-	return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error())
-}
-
-func isExecError(err error) (execError, bool) {
-	if exErr, ok := err.(execError); ok {
-		return exErr, true
-	}
-
-	fields := strings.Split(err.Error(), ": ")
-	knownTypes := []string{
-		"not-found",
-		"not-executable",
-	}
-
-	if len(fields) > 1 && contains(knownTypes, fields[1]) {
-		var colon bool
-		var e error
-		switch fields[1] {
-			case "not-found":
-				e = errNotFound
-			case "not-executable":
-				colon = true
-				e = errNotExec
-		}
-
-		return execError{
-			cmd: fields[0],
-			typ: fields[1],
-			colon: colon,
-			err: e,
-		}, true
-	}
-
-	return execError{}, false
-}
-
 func runInput(input string, priv bool) {
 	running = true
 	cmdString := aliases.Resolve(input)
 	hooks.Emit("command.preexec", input, cmdString)
 
+	currentRunner := runnerMode
+
 	rerun:
 	var exitCode uint8
-	var err error
 	var cont bool
 	var newline bool
 	// save incase it changes while prompting (For some reason)
-	currentRunner := runnerMode
-	if currentRunner.Type() == rt.StringType {
-		switch currentRunner.AsString() {
-			case "hybrid":
-				_, _, err = handleLua(input)
-				if err == nil {
-					cmdFinish(0, input, priv)
-					return
-				}
-				input, exitCode, cont, newline, err = handleSh(input)
-			case "hybridRev":
-				_, _, _, _, err = handleSh(input)
-				if err == nil {
-					cmdFinish(0, input, priv)
-					return
-				}
-				input, exitCode, err = handleLua(input)
-			case "lua":
-				input, exitCode, err = handleLua(input)
-			case "sh":
-				input, exitCode, cont, newline, err = handleSh(input)
-		}
-	} else {
-		// can only be a string or function so
-		var runnerErr error
-		input, exitCode, cont, newline, runnerErr, err = runLuaRunner(currentRunner, input)
-		if err != nil {
-			fmt.Fprintln(os.Stderr, err)
-			cmdFinish(124, input, priv)
-			return
-		}
-		// yep, we only use `err` to check for lua eval error
-		// our actual error should only be a runner provided error at this point
-		// command not found type, etc
-		err = runnerErr
+	input, exitCode, cont, newline, runnerErr, err := runLuaRunner(currentRunner, input)
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		cmdFinish(124, input, priv)
+		return
 	}
+	// we only use `err` to check for lua eval error
+	// our actual error should only be a runner provided error at this point
+	// command not found type, etc
+	err = runnerErr
 
 	if cont {
 		input, err = continuePrompt(input, newline)
@@ -147,8 +51,8 @@ func runInput(input string, priv bool) {
 	}
 
 	if err != nil && err != io.EOF {
-		if exErr, ok := isExecError(err); ok {
-			hooks.Emit("command." + exErr.typ, exErr.cmd)
+		if exErr, ok := util.IsExecError(err); ok {
+			hooks.Emit("command." + exErr.Typ, exErr.Cmd)
 		} else {
 			fmt.Fprintln(os.Stderr, err)
 		}
@@ -239,16 +143,7 @@ func handleLua(input string) (string, uint8, error) {
 	return cmdString, 125, err
 }
 
-func handleSh(cmdString string) (input string, exitCode uint8, cont bool, newline bool, runErr error) {
-	shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh"))
-	var err error
-	input, exitCode, cont, newline, runErr, err = runLuaRunner(shRunner, cmdString)
-	if err != nil {
-		runErr = err
-	}
-	return
-}
-
+/*
 func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline bool, e error) {
 	_, _, err := execCommand(cmdString, nil)
 	if err != nil {
@@ -274,256 +169,7 @@ func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline
 
 	return cmdString, 0, false, false, nil
 }
-
-// Run command in sh interpreter
-func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) {
-	file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
-	if err != nil {
-		return nil, nil, err
-	}
-
-	if strms == nil {
-		strms = &streams{}
-	}
-
-	if strms.stdout == nil {
-		strms.stdout = os.Stdout
-	}
-
-	if strms.stderr == nil {
-		strms.stderr = os.Stderr
-	}
-
-	if strms.stdin == nil {
-		strms.stdin = os.Stdin
-	}
-
-	interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner)
-	interp.Env(nil)(runner)
-
-	buf := new(bytes.Buffer)
-	printer := syntax.NewPrinter()
-
-	var bg bool
-	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, []string{}, "")
-		}
-
-		interp.ExecHandler(execHandle(bg))(runner)
-		err = runner.Run(context.TODO(), stmt)
-		if err != nil {
-			return strms.stdout, strms.stderr, err
-		}
-	}
-
-	return strms.stdout, strms.stderr, nil
-}
-
-func execHandle(bg bool) interp.ExecHandlerFunc {
-	return func(ctx context.Context, args []string) error {
-		_, argstring := splitInput(strings.Join(args, " "))
-		// i dont really like this but it works
-		if aliases.All()[args[0]] != "" {
-			for i, arg := range args {
-				if strings.Contains(arg, " ") {
-					args[i] = fmt.Sprintf("\"%s\"", arg)
-				}
-			}
-			_, argstring = splitInput(strings.Join(args, " "))
-
-			// If alias was found, use command alias
-			argstring = aliases.Resolve(argstring)
-			var err error
-			args, err = shell.Fields(argstring, nil)
-			if err != nil {
-				return err
-			}
-		}
-
-		// If command is defined in Lua then run it
-		luacmdArgs := rt.NewTable()
-		for i, str := range args[1:] {
-			luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
-		}
-
-		hc := interp.HandlerCtx(ctx)
-		if cmd := cmds.Commands[args[0]]; cmd != nil {
-			stdin := newSinkInput(hc.Stdin)
-			stdout := newSinkOutput(hc.Stdout)
-			stderr := newSinkOutput(hc.Stderr)
-
-			sinks := rt.NewTable()
-			sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud))
-			sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud))
-			sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud))
-			sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud))
-
-			t := rt.NewThread(l)
-			sig := make(chan os.Signal)
-			exit := make(chan bool)
-
-			luaexitcode := rt.IntValue(63)
-			var err error
-			go func() {
-				defer func() {
-					if r := recover(); r != nil {
-						exit <- true
-					}
-				}()
-
-				signal.Notify(sig, os.Interrupt)
-				select {
-					case <-sig:
-						t.KillContext()
-						return
-				}
-
-			}()
-
-			go func() {
-				luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
-				exit <- true
-			}()
-
-			<-exit
-			if err != nil {
-				fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
-				return interp.NewExitStatus(1)
-			}
-
-			var exitcode uint8
-
-			if code, ok := luaexitcode.TryInt(); ok {
-				exitcode = uint8(code)
-			} else if luaexitcode != rt.NilValue {
-				// deregister commander
-				delete(cmds.Commands, args[0])
-				fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
-			}
-
-			return interp.NewExitStatus(exitcode)
-		}
-
-		path, err := util.LookPath(args[0])
-		if err == errNotExec {
-			return execError{
-				typ: "not-executable",
-				cmd: args[0],
-				code: 126,
-				colon: true,
-				err: errNotExec,
-			}
-		} else if err != nil {
-			return execError{
-				typ: "not-found",
-				cmd: args[0],
-				code: 127,
-				err: errNotFound,
-			}
-		}
-
-		killTimeout := 2 * time.Second
-		// from here is basically copy-paste of the default exec handler from
-		// sh/interp but with our job handling
-
-		env := hc.Env
-		envList := os.Environ()
-		env.Each(func(name string, vr expand.Variable) bool {
-			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,
-		}
-
-		var j *job
-		if bg {
-			j = jobs.getLatest()
-			j.setHandle(&cmd)
-			err = j.start()
-		} else {
-			err = cmd.Start()
-		}
-
-		if err == nil {
-			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()
-		}
-
-		exit := handleExecErr(err)
-
-		if bg {
-			j.exitCode = int(exit)
-			j.finish()
-		}
-		return interp.NewExitStatus(exit)
-	}
-}
-
-func handleExecErr(err error) (exit uint8) {
-	ctx := context.TODO()
-
-	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
-				}
-				exit = uint8(128 + status.Signal())
-				return
-			}
-			exit = uint8(status.ExitStatus())
-			return
-		}
-		exit = 1
-		return
-	case *exec.Error:
-		// did not start
-		//fmt.Fprintf(hc.Stderr, "%v\n", err)
-		exit = 127
-	default: return
-	}
-
-	return
-}
+*/
 
 func splitInput(input string) ([]string, string) {
 	// end my suffering
diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go
new file mode 100644
index 0000000..40fad70
--- /dev/null
+++ b/golibs/snail/lua.go
@@ -0,0 +1,125 @@
+package snail
+
+import (
+	"fmt"
+	"strings"
+
+	"hilbish/util"
+
+	rt "github.com/arnodel/golua/runtime"
+	"github.com/arnodel/golua/lib/packagelib"
+	"mvdan.cc/sh/v3/interp"
+	"mvdan.cc/sh/v3/syntax"
+)
+
+var snailMetaKey = rt.StringValue("hshsnail")
+var Loader = packagelib.Loader{
+	Load: loaderFunc,
+	Name: "fs",
+}
+
+func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
+	snailMeta := rt.NewTable()
+	snailMethods := rt.NewTable()
+	snailFuncs := map[string]util.LuaExport{
+		"run": {srun, 1, false},
+	}
+	util.SetExports(rtm, snailMethods, snailFuncs)
+
+	snailIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+		arg := c.Arg(1)
+		val := snailMethods.Get(arg)
+
+		return c.PushingNext1(t.Runtime, val), nil
+	}
+	snailMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(snailIndex, "__index", 2, false)))
+	rtm.SetRegistry(snailMetaKey, rt.TableValue(snailMeta))
+
+	exports := map[string]util.LuaExport{
+		"new": util.LuaExport{snew, 0, false},
+	}
+
+	mod := rt.NewTable()
+	util.SetExports(rtm, mod, exports)
+
+	return rt.TableValue(mod), nil
+}
+
+func snew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+	s := New(t.Runtime)
+	return c.PushingNext1(t.Runtime, rt.UserDataValue(snailUserData(s))), nil
+}
+
+func srun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+	if err := c.CheckNArgs(2); err != nil {
+		return nil, err
+	}
+
+	s, err := snailArg(c, 0)
+	if err != nil {
+		return nil, err
+	}
+
+	cmd, err := c.StringArg(1)
+	if err != nil {
+		return nil, err
+	}
+
+	var newline bool
+	var cont bool
+	var luaErr rt.Value = rt.NilValue
+	exitCode := 0
+	bg, _, _, err := s.Run(cmd, nil)
+	if err != nil {
+		if syntax.IsIncomplete(err) {
+			/*
+			if !interactive {
+				return cmdString, 126, false, false, err
+			}
+			*/
+			if strings.Contains(err.Error(), "unclosed here-document") {
+				newline = true
+			}
+			cont = true
+		} else {
+			if code, ok := interp.IsExitStatus(err); ok {
+				exitCode = int(code)
+			} else {
+				luaErr = rt.StringValue(err.Error())
+			}
+		}
+	}
+	runnerRet := rt.NewTable()
+	runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd))
+	runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode)))
+	runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont))
+	runnerRet.Set(rt.StringValue("newline"), rt.BoolValue(newline))
+	runnerRet.Set(rt.StringValue("err"), luaErr)
+
+	runnerRet.Set(rt.StringValue("bg"), rt.BoolValue(bg))
+	return c.PushingNext1(t.Runtime, rt.TableValue(runnerRet)), nil
+}
+
+func snailArg(c *rt.GoCont, arg int) (*snail, error) {
+	s, ok := valueToSnail(c.Arg(arg))
+	if !ok {
+		return nil, fmt.Errorf("#%d must be a snail", arg + 1)
+	}
+
+	return s, nil
+}
+
+func valueToSnail(val rt.Value) (*snail, bool) {
+	u, ok := val.TryUserData()
+	if !ok {
+		return nil, false
+	}
+
+	s, ok := u.Value().(*snail)
+	return s, ok
+}
+
+func snailUserData(s *snail) *rt.UserData {
+	snailMeta := s.runtime.Registry(snailMetaKey)
+	return rt.NewUserData(s, snailMeta.AsTable())
+}
diff --git a/golibs/snail/snail.go b/golibs/snail/snail.go
new file mode 100644
index 0000000..4ef92ea
--- /dev/null
+++ b/golibs/snail/snail.go
@@ -0,0 +1,302 @@
+// shell script interpreter library
+package snail
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"os/signal"
+	"runtime"
+	"strings"
+	"time"
+
+	"hilbish/sink"
+	"hilbish/util"
+
+	rt "github.com/arnodel/golua/runtime"
+	"mvdan.cc/sh/v3/shell"
+	//"github.com/yuin/gopher-lua/parse"
+	"mvdan.cc/sh/v3/interp"
+	"mvdan.cc/sh/v3/syntax"
+	"mvdan.cc/sh/v3/expand"
+)
+
+type snail struct{
+	runner *interp.Runner
+	runtime *rt.Runtime
+}
+
+func New(rtm *rt.Runtime) *snail {
+	runner, _ := interp.New()
+
+	return &snail{
+		runner: runner,
+		runtime: rtm,
+	}
+}
+
+func (s *snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer, error){
+	file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
+	if err != nil {
+		return false, nil, nil, err
+	}
+
+	if strms == nil {
+		strms = &util.Streams{}
+	}
+
+	if strms.Stdout == nil {
+		strms.Stdout = os.Stdout
+	}
+
+	if strms.Stderr == nil {
+		strms.Stderr = os.Stderr
+	}
+
+	if strms.Stdin == nil {
+		strms.Stdin = os.Stdin
+	}
+
+	interp.StdIO(strms.Stdin, strms.Stdout, strms.Stderr)(s.runner)
+	interp.Env(nil)(s.runner)
+
+	buf := new(bytes.Buffer)
+	//printer := syntax.NewPrinter()
+
+	var bg bool
+	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, []string{}, "")
+		}
+
+		interp.ExecHandler(func(ctx context.Context, args []string) error {
+			_, argstring := splitInput(strings.Join(args, " "))
+			// i dont really like this but it works
+			aliases := make(map[string]string)
+			aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.all()")
+			util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) {
+				aliases[k.AsString()] = v.AsString()
+			})
+			if aliases[args[0]] != "" {
+				for i, arg := range args {
+					if strings.Contains(arg, " ") {
+						args[i] = fmt.Sprintf("\"%s\"", arg)
+					}
+				}
+				_, argstring = splitInput(strings.Join(args, " "))
+
+				// If alias was found, use command alias
+				argstring = util.MustDoString(s.runtime, fmt.Sprintf(`return hilbish.aliases.resolve("%s")`, argstring)).AsString()
+
+				var err error
+				args, err = shell.Fields(argstring, nil)
+				if err != nil {
+					return err
+				}
+			}
+
+			// If command is defined in Lua then run it
+			luacmdArgs := rt.NewTable()
+			for i, str := range args[1:] {
+				luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
+			}
+
+			hc := interp.HandlerCtx(ctx)
+
+			cmds := make(map[string]*rt.Closure)
+			luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable()
+			util.ForEach(luaCmds, func(k, v rt.Value) {
+				cmds[k.AsString()] = k.AsTable().Get(rt.StringValue("exec")).AsClosure()
+			})
+			if cmd := cmds[args[0]]; cmd != nil {
+				stdin := sink.NewSinkInput(s.runtime, hc.Stdin)
+				stdout := sink.NewSinkOutput(s.runtime, hc.Stdout)
+				stderr := sink.NewSinkOutput(s.runtime, hc.Stderr)
+
+				sinks := rt.NewTable()
+				sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.UserData))
+				sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.UserData))
+				sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.UserData))
+				sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.UserData))
+
+				t := rt.NewThread(s.runtime)
+				sig := make(chan os.Signal)
+				exit := make(chan bool)
+
+				luaexitcode := rt.IntValue(63)
+				var err error
+				go func() {
+					defer func() {
+						if r := recover(); r != nil {
+							exit <- true
+						}
+					}()
+
+					signal.Notify(sig, os.Interrupt)
+					select {
+						case <-sig:
+							t.KillContext()
+							return
+					}
+
+				}()
+
+				go func() {
+					luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
+					exit <- true
+				}()
+
+				<-exit
+				if err != nil {
+					fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
+					return interp.NewExitStatus(1)
+				}
+
+				var exitcode uint8
+
+				if code, ok := luaexitcode.TryInt(); ok {
+					exitcode = uint8(code)
+				} else if luaexitcode != rt.NilValue {
+					// deregister commander
+					delete(cmds, args[0])
+					fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
+				}
+
+				return interp.NewExitStatus(exitcode)
+			}
+
+			path, err := util.LookPath(args[0])
+			if err == util.ErrNotExec {
+				return util.ExecError{
+					Typ: "not-executable",
+					Cmd: args[0],
+					Code: 126,
+					Colon: true,
+					Err: util.ErrNotExec,
+				}
+			} else if err != nil {
+				return util.ExecError{
+					Typ: "not-found",
+					Cmd: args[0],
+					Code: 127,
+					Err: util.ErrNotFound,
+				}
+			}
+
+			killTimeout := 2 * time.Second
+			// from here is basically copy-paste of the default exec handler from
+			// sh/interp but with our job handling
+
+			env := hc.Env
+			envList := os.Environ()
+			env.Each(func(name string, vr expand.Variable) bool {
+				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,
+			}
+
+			//var j *job
+			if bg {
+				/*
+				j = jobs.getLatest()
+				j.setHandle(&cmd)
+				err = j.start()
+				*/
+			} else {
+				err = cmd.Start()
+			}
+
+			if err == nil {
+				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()
+			}
+
+			exit := util.HandleExecErr(err)
+
+			if bg {
+				//j.exitCode = int(exit)
+				//j.finish()
+			}
+			return interp.NewExitStatus(exit)
+		})(s.runner)
+		err = s.runner.Run(context.TODO(), stmt)
+		if err != nil {
+			return bg, strms.Stdout, strms.Stderr, err
+		}
+	}
+
+	return bg, strms.Stdout, strms.Stderr, nil
+}
+
+func splitInput(input string) ([]string, string) {
+	// end my suffering
+	// TODO: refactor this garbage
+	quoted := false
+	cmdArgs := []string{}
+	sb := &strings.Builder{}
+	cmdstr := &strings.Builder{}
+
+	for _, r := range input {
+		if r == '"' {
+			// start quoted input
+			// this determines if other runes are replaced
+			quoted = !quoted
+			// dont add back quotes
+			//sb.WriteRune(r)
+		} else if !quoted && r == '~' {
+			// if not in quotes and ~ is found then make it $HOME
+			sb.WriteString(os.Getenv("HOME"))
+		} else if !quoted && r == ' ' {
+			// if not quoted and there's a space then add to cmdargs
+			cmdArgs = append(cmdArgs, sb.String())
+			sb.Reset()
+		} else {
+			sb.WriteRune(r)
+		}
+		cmdstr.WriteRune(r)
+	}
+	if sb.Len() > 0 {
+		cmdArgs = append(cmdArgs, sb.String())
+	}
+
+	return cmdArgs, cmdstr.String()
+}
diff --git a/golibs/terminal/snail.go b/golibs/terminal/snail.go
deleted file mode 100644
index e69de29..0000000
diff --git a/job.go b/job.go
index bb16e92..fcb1c2c 100644
--- a/job.go
+++ b/job.go
@@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 
 	if !j.running {
 		err := j.start()
-		exit := handleExecErr(err)
+		exit := util.HandleExecErr(err)
 		j.exitCode = int(exit)
 		j.finish()
 	}
diff --git a/lua.go b/lua.go
index 94b7910..9cefada 100644
--- a/lua.go
+++ b/lua.go
@@ -4,10 +4,12 @@ import (
 	"fmt"
 	"os"
 
+	"hilbish/sink"
 	"hilbish/util"
 	"hilbish/golibs/bait"
 	"hilbish/golibs/commander"
 	"hilbish/golibs/fs"
+	"hilbish/golibs/snail"
 	"hilbish/golibs/terminal"
 
 	rt "github.com/arnodel/golua/runtime"
@@ -23,15 +25,15 @@ func luaInit() {
 		MessageHandler: debuglib.Traceback,
 	})
 	lib.LoadAll(l)
-	setupSinkType(l)
+	sink.SetupSinkType(l)
 
 	lib.LoadLibs(l, hilbishLoader)
 	// yes this is stupid, i know
 	util.DoString(l, "hilbish = require 'hilbish'")
 
-	// Add fs and terminal module module to Lua
 	lib.LoadLibs(l, fs.Loader)
 	lib.LoadLibs(l, terminal.Loader)
+	lib.LoadLibs(l, snail.Loader)
 
 	cmds = commander.New(l)
 	lib.LoadLibs(l, cmds.Loader)
diff --git a/main.go b/main.go
index af7a22a..c26a55e 100644
--- a/main.go
+++ b/main.go
@@ -21,7 +21,6 @@ import (
 	"github.com/pborman/getopt"
 	"github.com/maxlandon/readline"
 	"golang.org/x/term"
-	"mvdan.cc/sh/v3/interp"
 )
 
 var (
@@ -311,15 +310,6 @@ func removeDupes(slice []string) []string {
 	return newSlice
 }
 
-func contains(s []string, e string) bool {
-	for _, a := range s {
-		if strings.ToLower(a) == strings.ToLower(e) {
-			return true
-		}
-	}
-	return false
-}
-
 func exit(code int) {
 	jobs.stopAll()
 
diff --git a/nature/runner.lua b/nature/runner.lua
index 235ab77..3d2bb61 100644
--- a/nature/runner.lua
+++ b/nature/runner.lua
@@ -1,4 +1,6 @@
 --- hilbish.runner
+local snail = require 'snail'
+
 local currentRunner = 'hybrid'
 local runners = {}
 
@@ -107,7 +109,5 @@ hilbish.runner.add('lua', function(input)
 	return hilbish.runner.lua(cmdStr)
 end)
 
-hilbish.runner.add('sh', function(input)
-	return hilbish.runner.sh(input)
-end)
+hilbish.runner.add('sh', snail.new())
 
diff --git a/runnermode.go b/runnermode.go
index fb8bcf4..f1e2bf0 100644
--- a/runnermode.go
+++ b/runnermode.go
@@ -53,7 +53,7 @@ end)
 */
 func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
 	exports := map[string]util.LuaExport{
-		"sh": {shRunner, 1, false},
+		//"sh": {shRunner, 1, false},
 		"lua": {luaRunner, 1, false},
 		"setMode": {hlrunnerMode, 1, false},
 	}
@@ -66,10 +66,12 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
 
 // #interface runner
 // setMode(cb)
+// **NOTE: This function is deprecated and will be removed in 3.0**
+// Use `hilbish.runner.setCurrent` instead.
 // This is the same as the `hilbish.runnerMode` function.
 // It takes a callback, which will be used to execute all interactive input.
 // In normal cases, neither callbacks should be overrided by the user,
-// as the higher level functions listed below this will handle it.
+// as the higher level functions (setCurrent) this will handle it.
 // #param cb function
 func _runnerMode() {}
 
@@ -78,6 +80,7 @@ func _runnerMode() {}
 // Runs a command in Hilbish's shell script interpreter.
 // This is the equivalent of using `source`.
 // #param cmd string
+/*
 func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	if err := c.Check1Arg(); err != nil {
 		return nil, err
@@ -101,6 +104,7 @@ func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 
 	return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil
 }
+*/
 
 // #interface runner
 // lua(cmd)
diff --git a/sink.go b/sink/sink.go
similarity index 87%
rename from sink.go
rename to sink/sink.go
index 3aa5507..be8b7d1 100644
--- a/sink.go
+++ b/sink/sink.go
@@ -1,4 +1,4 @@
-package main
+package sink
 
 import (
 	"bufio"
@@ -17,15 +17,15 @@ var sinkMetaKey = rt.StringValue("hshsink")
 // #type
 // A sink is a structure that has input and/or output to/from
 // a desination.
-type sink struct{
+type Sink struct{
 	writer *bufio.Writer
 	reader *bufio.Reader
 	file *os.File
-	ud *rt.UserData
+	UserData *rt.UserData
 	autoFlush bool
 }
 
-func setupSinkType(rtm *rt.Runtime) {
+func SetupSinkType(rtm *rt.Runtime) {
 	sinkMeta := rt.NewTable()
 
 	sinkMethods := rt.NewTable()
@@ -37,7 +37,7 @@ func setupSinkType(rtm *rt.Runtime) {
 		"write": {luaSinkWrite, 2, false},
 		"writeln": {luaSinkWriteln, 2, false},
 	}
-	util.SetExports(l, sinkMethods, sinkFuncs)
+	util.SetExports(rtm, sinkMethods, sinkFuncs)
 
 	sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		s, _ := sinkArg(c, 0)
@@ -64,7 +64,7 @@ func setupSinkType(rtm *rt.Runtime) {
 	}
 
 	sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false)))
-	l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
+	rtm.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
 }
 
 
@@ -212,11 +212,11 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	return c.Next(), nil
 }
 
-func newSinkInput(r io.Reader) *sink {
-	s := &sink{
+func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
+	s := &Sink{
 		reader: bufio.NewReader(r),
 	}
-	s.ud = sinkUserData(s)
+	s.UserData = sinkUserData(rtm, s)
 
 	if f, ok := r.(*os.File); ok {
 		s.file = f
@@ -225,17 +225,17 @@ func newSinkInput(r io.Reader) *sink {
 	return s
 }
 
-func newSinkOutput(w io.Writer) *sink {
-	s := &sink{
+func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink {
+	s := &Sink{
 		writer: bufio.NewWriter(w),
 		autoFlush: true,
 	}
-	s.ud = sinkUserData(s)
+	s.UserData = sinkUserData(rtm, s)
 
 	return s
 }
 
-func sinkArg(c *rt.GoCont, arg int) (*sink, error) {
+func sinkArg(c *rt.GoCont, arg int) (*Sink, error) {
 	s, ok := valueToSink(c.Arg(arg))
 	if !ok {
 		return nil, fmt.Errorf("#%d must be a sink", arg + 1)
@@ -244,17 +244,17 @@ func sinkArg(c *rt.GoCont, arg int) (*sink, error) {
 	return s, nil
 }
 
-func valueToSink(val rt.Value) (*sink, bool) {
+func valueToSink(val rt.Value) (*Sink, bool) {
 	u, ok := val.TryUserData()
 	if !ok {
 		return nil, false
 	}
 
-	s, ok := u.Value().(*sink)
+	s, ok := u.Value().(*Sink)
 	return s, ok
 }
 
-func sinkUserData(s *sink) *rt.UserData {
-	sinkMeta := l.Registry(sinkMetaKey)
+func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData {
+	sinkMeta := rtm.Registry(sinkMetaKey)
 	return rt.NewUserData(s, sinkMeta.AsTable())
 }
diff --git a/util/streams.go b/util/streams.go
new file mode 100644
index 0000000..11f9308
--- /dev/null
+++ b/util/streams.go
@@ -0,0 +1,11 @@
+package util
+
+import (
+	"io"
+)
+
+type Streams struct {
+	Stdout io.Writer
+	Stderr io.Writer
+	Stdin io.Reader
+}
diff --git a/util/util.go b/util/util.go
index e60f66e..b32d865 100644
--- a/util/util.go
+++ b/util/util.go
@@ -2,14 +2,78 @@ package util
 
 import (
 	"bufio"
+	"context"
+	"errors"
+	"fmt"
 	"io"
+	"path/filepath"
 	"strings"
 	"os"
+	"os/exec"
 	"os/user"
+	"runtime"
+	"syscall"
 
 	rt "github.com/arnodel/golua/runtime"
 )
 
+var ErrNotExec = errors.New("not executable")
+var ErrNotFound = errors.New("not found")
+
+type ExecError struct{
+	Typ string
+	Cmd string
+	Code int
+	Colon bool
+	Err error
+}
+
+func (e ExecError) Error() string {
+	return fmt.Sprintf("%s: %s", e.Cmd, e.Typ)
+}
+
+func (e ExecError) sprint() error {
+	sep := " "
+	if e.Colon {
+		sep = ": "
+	}
+
+	return fmt.Errorf("hilbish: %s%s%s", e.Cmd, sep, e.Err.Error())
+}
+
+func IsExecError(err error) (ExecError, bool) {
+	if exErr, ok := err.(ExecError); ok {
+		return exErr, true
+	}
+
+	fields := strings.Split(err.Error(), ": ")
+	knownTypes := []string{
+		"not-found",
+		"not-executable",
+	}
+
+	if len(fields) > 1 && Contains(knownTypes, fields[1]) {
+		var colon bool
+		var e error
+		switch fields[1] {
+			case "not-found":
+				e = ErrNotFound
+			case "not-executable":
+				colon = true
+				e = ErrNotExec
+		}
+
+		return ExecError{
+			Cmd: fields[0],
+			Typ: fields[1],
+			Colon: colon,
+			Err: e,
+		}, true
+	}
+
+	return ExecError{}, false
+}
+
 // SetField sets a field in a table, adding docs for it.
 // It is accessible via the __docProp metatable. It is a table of the names of the fields.
 func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value) {
@@ -36,6 +100,15 @@ func DoString(rtm *rt.Runtime, code string) (rt.Value, error) {
 	return ret, err
 }
 
+func MustDoString(rtm *rt.Runtime, code string) rt.Value {
+	val, err := DoString(rtm, code)
+	if err != nil {
+		panic(err)
+	}
+
+	return val
+}
+
 // DoFile runs the contents of the file in the Lua runtime.
 func DoFile(rtm *rt.Runtime, path string) error {
 	f, err := os.Open(path)
@@ -151,13 +224,13 @@ func LookPath(file string) (string, error) { // custom lookpath function so we k
 	}
 	for _, s := range skip {
 		if strings.HasPrefix(file, s) {
-			return file, findExecutable(file, false, false)
+			return file, FindExecutable(file, false, false)
 		}
 	}
 	for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
 		path := filepath.Join(dir, file)
-		err := findExecutable(path, true, false)
-		if err == errNotExec {
+		err := FindExecutable(path, true, false)
+		if err == ErrNotExec {
 			return "", err
 		} else if err == nil {
 			return path, nil
@@ -166,3 +239,42 @@ func LookPath(file string) (string, error) { // custom lookpath function so we k
 
 	return "", os.ErrNotExist
 }
+
+func Contains(s []string, e string) bool {
+	for _, a := range s {
+		if strings.ToLower(a) == strings.ToLower(e) {
+			return true
+		}
+	}
+	return false
+}
+
+func HandleExecErr(err error) (exit uint8) {
+	ctx := context.TODO()
+
+	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
+				}
+				exit = uint8(128 + status.Signal())
+				return
+			}
+			exit = uint8(status.ExitStatus())
+			return
+		}
+		exit = 1
+		return
+	case *exec.Error:
+		// did not start
+		//fmt.Fprintf(hc.Stderr, "%v\n", err)
+		exit = 127
+	default: return
+	}
+
+	return
+}
diff --git a/util/util_unix.go b/util/util_unix.go
index 9fa6a6c..92813c8 100644
--- a/util/util_unix.go
+++ b/util/util_unix.go
@@ -20,5 +20,5 @@ func FindExecutable(path string, inPath, dirs bool) error {
 			return nil
 		}
 	}
-	return errNotExec
+	return ErrNotExec
 }

From 1379df7c2403ea92c8909e79a5f36a6ab2ea214b Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:18:45 -0400
Subject: [PATCH 04/14] fix: dont override fs lib on snail

---
 golibs/snail/lua.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go
index 40fad70..ca4e3e9 100644
--- a/golibs/snail/lua.go
+++ b/golibs/snail/lua.go
@@ -15,7 +15,7 @@ import (
 var snailMetaKey = rt.StringValue("hshsnail")
 var Loader = packagelib.Loader{
 	Load: loaderFunc,
-	Name: "fs",
+	Name: "snail",
 }
 
 func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {

From 68fc973bfb5bf0644c51e1c013c7586eee5f67fd Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:35:01 -0400
Subject: [PATCH 05/14] fix: make runnerMode nil

---
 exec.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/exec.go b/exec.go
index 19a5d2f..9819d15 100644
--- a/exec.go
+++ b/exec.go
@@ -16,7 +16,7 @@ import (
 
 var errNotExec = errors.New("not executable")
 var errNotFound = errors.New("not found")
-var runnerMode rt.Value = rt.StringValue("hybrid")
+var runnerMode rt.Value = rt.NilValue
 
 func runInput(input string, priv bool) {
 	running = true

From 16ddb85b4f5edfdd6019ce18fd2c1ecca0acca43 Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:35:26 -0400
Subject: [PATCH 06/14] fix: make snail.run accept correct number of args

---
 golibs/snail/lua.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go
index ca4e3e9..db2ff07 100644
--- a/golibs/snail/lua.go
+++ b/golibs/snail/lua.go
@@ -22,7 +22,7 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
 	snailMeta := rt.NewTable()
 	snailMethods := rt.NewTable()
 	snailFuncs := map[string]util.LuaExport{
-		"run": {srun, 1, false},
+		"run": {srun, 2, false},
 	}
 	util.SetExports(rtm, snailMethods, snailFuncs)
 

From 55e879bd028f4004d031955b1c59230afcd43e93 Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:36:33 -0400
Subject: [PATCH 07/14] fix: typos, set default runner to lua implemented
 hybrid, add back hilbish.runner.sh

---
 golibs/snail/snail.go | 4 ++--
 nature/runner.lua     | 9 +++++++--
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/golibs/snail/snail.go b/golibs/snail/snail.go
index 4ef92ea..ddfef49 100644
--- a/golibs/snail/snail.go
+++ b/golibs/snail/snail.go
@@ -82,7 +82,7 @@ func (s *snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer
 			_, argstring := splitInput(strings.Join(args, " "))
 			// i dont really like this but it works
 			aliases := make(map[string]string)
-			aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.all()")
+			aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.list()")
 			util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) {
 				aliases[k.AsString()] = v.AsString()
 			})
@@ -115,7 +115,7 @@ func (s *snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer
 			cmds := make(map[string]*rt.Closure)
 			luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable()
 			util.ForEach(luaCmds, func(k, v rt.Value) {
-				cmds[k.AsString()] = k.AsTable().Get(rt.StringValue("exec")).AsClosure()
+				cmds[k.AsString()] = v.AsTable().Get(rt.StringValue("exec")).AsClosure()
 			})
 			if cmd := cmds[args[0]]; cmd != nil {
 				stdin := sink.NewSinkInput(s.runtime, hc.Stdin)
diff --git a/nature/runner.lua b/nature/runner.lua
index 3d2bb61..9ece224 100644
--- a/nature/runner.lua
+++ b/nature/runner.lua
@@ -83,6 +83,11 @@ function hilbish.runner.getCurrent()
 	return currentRunner
 end
 
+local snaili = snail.new()
+function hilbish.runner.sh(input)
+	return snaili:run(input)
+end
+
 hilbish.runner.add('hybrid', function(input)
 	local cmdStr = hilbish.aliases.resolve(input)
 
@@ -109,5 +114,5 @@ hilbish.runner.add('lua', function(input)
 	return hilbish.runner.lua(cmdStr)
 end)
 
-hilbish.runner.add('sh', snail.new())
-
+hilbish.runner.add('sh', hilbish.runner.sh)
+hilbish.runner.setCurrent 'hybrid'

From a8435b649dc0b6bfd9151d3c8d823c6de2f2461c Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:44:24 -0400
Subject: [PATCH 08/14] fix: use error code if err is ExecError

---
 golibs/snail/lua.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go
index db2ff07..61ca254 100644
--- a/golibs/snail/lua.go
+++ b/golibs/snail/lua.go
@@ -85,6 +85,9 @@ func srun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 			if code, ok := interp.IsExitStatus(err); ok {
 				exitCode = int(code)
 			} else {
+				if exErr, ok := util.IsExecError(err); ok {
+					exitCode = exErr.Code
+				}
 				luaErr = rt.StringValue(err.Error())
 			}
 		}

From 7174f40b5f2cdce398d7979bfabdecac60d29cd5 Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:48:11 -0400
Subject: [PATCH 09/14] fix: undefined vars on windows

---
 util/util_windows.go | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/util/util_windows.go b/util/util_windows.go
index 4eb50c8..ab490ad 100644
--- a/util/util_windows.go
+++ b/util/util_windows.go
@@ -5,6 +5,8 @@ package util
 import (
 	"path/filepath"
 	"os"
+
+	"hilbish/util"
 )
 
 func FindExecutable(path string, inPath, dirs bool) error {
@@ -21,15 +23,15 @@ func FindExecutable(path string, inPath, dirs bool) error {
 		} else {
 			_, err := os.Stat(path)
 			if err == nil {
-				if contains(pathExts, nameExt) { return nil }
-				return errNotExec
+				if util.Contains(pathExts, nameExt) { return nil }
+				return util.ErrNotExec
 			}
 		}
 	} else {
 		_, err := os.Stat(path)
 		if err == nil {
-			if contains(pathExts, nameExt) { return nil }
-			return errNotExec
+			if util.Contains(pathExts, nameExt) { return nil }
+			return util.ErrNotExec
 		}
 	}
 

From d1934452bb985119369d077c89cb899d6fa2eeb6 Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 01:52:00 -0400
Subject: [PATCH 10/14] fix: dont import util in the util package (bruh)

---
 util/util_windows.go | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/util/util_windows.go b/util/util_windows.go
index ab490ad..3321033 100644
--- a/util/util_windows.go
+++ b/util/util_windows.go
@@ -5,8 +5,6 @@ package util
 import (
 	"path/filepath"
 	"os"
-
-	"hilbish/util"
 )
 
 func FindExecutable(path string, inPath, dirs bool) error {
@@ -23,15 +21,15 @@ func FindExecutable(path string, inPath, dirs bool) error {
 		} else {
 			_, err := os.Stat(path)
 			if err == nil {
-				if util.Contains(pathExts, nameExt) { return nil }
-				return util.ErrNotExec
+				if Contains(pathExts, nameExt) { return nil }
+				return ErrNotExec
 			}
 		}
 	} else {
 		_, err := os.Stat(path)
 		if err == nil {
-			if util.Contains(pathExts, nameExt) { return nil }
-			return util.ErrNotExec
+			if Contains(pathExts, nameExt) { return nil }
+			return ErrNotExec
 		}
 	}
 

From b84d985ce6d59149689a1239eee62607007b396f Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 11:28:14 -0400
Subject: [PATCH 11/14] fix: throw old cd hook

---
 nature/commands/cd.lua | 1 +
 1 file changed, 1 insertion(+)

diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua
index 0efbb3f..284d420 100644
--- a/nature/commands/cd.lua
+++ b/nature/commands/cd.lua
@@ -22,5 +22,6 @@ commander.register('cd', function (args, sinks)
 		sinks.out:writeln(err)
 		return 1
 	end
+	bait.throw('cd', path, oldPath)
 	bait.throw('hilbish.cd', fs.abs(path), oldPath)
 end)

From 56ba00e213fca2bf91f057a5c77b896f76f2537d Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 15:25:34 -0400
Subject: [PATCH 12/14] docs: fix hilbish.vimAction doc

---
 docs/hooks/hilbish.md | 28 ++++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/docs/hooks/hilbish.md b/docs/hooks/hilbish.md
index d5d8a48..038b721 100644
--- a/docs/hooks/hilbish.md
+++ b/docs/hooks/hilbish.md
@@ -43,5 +43,29 @@ The notification. The properties are defined in the link above.
 
 <hr>
 
-+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something
-like yanking or pasting text. See `doc vim-mode actions` for more info.
+## hilbish.cd
+Sent when the current directory of the shell is changed (via interactive means.)
+If you are implementing a custom command that changes the directory of the shell,
+you must throw this hook manually for correctness.
+
+#### Variables
+`string` **`path`**  
+Absolute path of the directory that was changed to.
+
+`string` **`oldPath`**  
+Absolute path of the directory Hilbish *was* in.
+
+<hr>
+
+## hilbish.vimAction
+Sent when the user does a "vim action," being something like yanking or pasting text.
+See `doc vim-mode actions` for more info.
+
+#### Variables
+`string` **`actionName`**  
+Absolute path of the directory that was changed to.
+
+`table` **`args`**  
+Table of args relating to the Vim action.
+
+<hr>

From 679c7b2974a09c89a32cc6a5938d8ba214b1e24a Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 19:21:54 -0400
Subject: [PATCH 13/14] feat: add hilbish.sink interface to create sinks

---
 api.go       |  4 ++++
 lua.go       |  2 --
 sink/sink.go | 52 ++++++++++++++++++++++++++++++++++++++++------------
 3 files changed, 44 insertions(+), 14 deletions(-)

diff --git a/api.go b/api.go
index d556589..eb1a49b 100644
--- a/api.go
+++ b/api.go
@@ -23,6 +23,7 @@ import (
 	"syscall"
 	"time"
 
+	"hilbish/sink"
 	"hilbish/util"
 
 	rt "github.com/arnodel/golua/runtime"
@@ -133,6 +134,9 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
 	pluginModule := moduleLoader(rtm)
 	mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule))
 
+	sinkModule := sink.Loader(l)
+	mod.Set(rt.StringValue("sink"), rt.TableValue(sinkModule))
+
 	return rt.TableValue(mod), nil
 }
 
diff --git a/lua.go b/lua.go
index 9cefada..00398ea 100644
--- a/lua.go
+++ b/lua.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"os"
 
-	"hilbish/sink"
 	"hilbish/util"
 	"hilbish/golibs/bait"
 	"hilbish/golibs/commander"
@@ -25,7 +24,6 @@ func luaInit() {
 		MessageHandler: debuglib.Traceback,
 	})
 	lib.LoadAll(l)
-	sink.SetupSinkType(l)
 
 	lib.LoadLibs(l, hilbishLoader)
 	// yes this is stupid, i know
diff --git a/sink/sink.go b/sink/sink.go
index be8b7d1..4899d89 100644
--- a/sink/sink.go
+++ b/sink/sink.go
@@ -2,6 +2,7 @@ package sink
 
 import (
 	"bufio"
+	"bytes"
 	"fmt"
 	"io"
 	"os"
@@ -18,14 +19,13 @@ var sinkMetaKey = rt.StringValue("hshsink")
 // A sink is a structure that has input and/or output to/from
 // a desination.
 type Sink struct{
-	writer *bufio.Writer
-	reader *bufio.Reader
+	rw *bufio.ReadWriter
 	file *os.File
 	UserData *rt.UserData
 	autoFlush bool
 }
 
-func SetupSinkType(rtm *rt.Runtime) {
+func Loader(rtm *rt.Runtime) *rt.Table {
 	sinkMeta := rt.NewTable()
 
 	sinkMethods := rt.NewTable()
@@ -65,9 +65,24 @@ func SetupSinkType(rtm *rt.Runtime) {
 
 	sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false)))
 	rtm.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
+
+	exports := map[string]util.LuaExport{
+		"new": {luaSinkNew, 0, false},
+	}
+
+	mod := rt.NewTable()
+	util.SetExports(rtm, mod, exports)
+
+	return mod
 }
 
 
+func luaSinkNew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+	snk := NewSink(t.Runtime, new(bytes.Buffer))
+
+	return c.PushingNext1(t.Runtime, rt.UserDataValue(snk.UserData)), nil
+}
+
 // #member
 // readAll() -> string
 // --- @returns string
@@ -84,7 +99,7 @@ func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 
 	lines := []string{}
 	for {
-		line, err := s.reader.ReadString('\n')
+		line, err := s.rw.ReadString('\n')
 		if err != nil {
 			if err == io.EOF {
 				break
@@ -113,7 +128,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	str, _ := s.reader.ReadString('\n')
+	str, _ := s.rw.ReadString('\n')
 
 	return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil
 }
@@ -135,9 +150,9 @@ func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	s.writer.Write([]byte(data))
+	s.rw.Write([]byte(data))
 	if s.autoFlush {
-		s.writer.Flush()
+		s.rw.Flush()
 	}
 
 	return c.Next(), nil
@@ -160,9 +175,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	s.writer.Write([]byte(data + "\n"))
+	s.rw.Write([]byte(data + "\n"))
 	if s.autoFlush {
-		s.writer.Flush()
+		s.rw.Flush()
 	}
 
 	return c.Next(), nil
@@ -181,7 +196,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	s.writer.Flush()
+	s.rw.Flush()
 
 	return c.Next(), nil
 }
@@ -212,9 +227,22 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	return c.Next(), nil
 }
 
+func NewSink(rtm *rt.Runtime, rw io.ReadWriter) *Sink {
+	s := &Sink{
+		rw: bufio.NewReadWriter(bufio.NewReader(rw), bufio.NewWriter(rw)),
+	}
+	s.UserData = sinkUserData(rtm, s)
+
+	if f, ok := rw.(*os.File); ok {
+		s.file = f
+	}
+
+	return s
+}
+
 func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
 	s := &Sink{
-		reader: bufio.NewReader(r),
+		rw: bufio.NewReadWriter(bufio.NewReader(r), nil),
 	}
 	s.UserData = sinkUserData(rtm, s)
 
@@ -227,7 +255,7 @@ func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
 
 func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink {
 	s := &Sink{
-		writer: bufio.NewWriter(w),
+		rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)),
 		autoFlush: true,
 	}
 	s.UserData = sinkUserData(rtm, s)

From 15e3c1a74b4e0c164b3ee88cf1bd51982ca39120 Mon Sep 17 00:00:00 2001
From: sammyette <torchedsammy@gmail.com>
Date: Sun, 29 Dec 2024 20:32:21 -0400
Subject: [PATCH 14/14] feat: reimplement hilbish.run in lua

---
 api.go              | 109 --------------------------------------------
 golibs/snail/lua.go |  59 +++++++++++++++++++++++-
 nature/hilbish.lua  |  30 ++++++++++++
 nature/init.lua     |   2 +
 nature/runner.lua   |   3 +-
 sink/sink.go        |  26 +++++------
 6 files changed, 103 insertions(+), 126 deletions(-)
 create mode 100644 nature/hilbish.lua

diff --git a/api.go b/api.go
index eb1a49b..8c14936 100644
--- a/api.go
+++ b/api.go
@@ -49,7 +49,6 @@ var exports = map[string]util.LuaExport{
 	"inputMode": {hlinputMode, 1, false},
 	"interval": {hlinterval, 2, false},
 	"read": {hlread, 1, false},
-	//"run": {hlrun, 1, true},
 	"timeout": {hltimeout, 2, false},
 	"which": {hlwhich, 1, false},
 }
@@ -188,114 +187,6 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error {
 }
 */
 
-// run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
-// Runs `cmd` in Hilbish's shell script interpreter.
-// The `streams` parameter specifies the output and input streams the command should use.
-// For example, to write command output to a sink.
-// As a table, the caller can directly specify the standard output, error, and input
-// streams of the command with the table keys `out`, `err`, and `input` respectively.
-// As a boolean, it specifies whether the command should use standard output or return its output streams.
-// #param cmd string
-// #param streams table|boolean
-// #returns number, string, string
-// #example
-/*
-// This code is the same as `ls -l | wc -l`
-local fs = require 'fs'
-local pr, pw = fs.pipe()
-hilbish.run('ls -l', {
-	stdout = pw,
-	stderr = pw,
-})
-
-pw:close()
-
-hilbish.run('wc -l', {
-	stdin = pr
-})
-*/
-// #example
-/*
-func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
-	// TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN.
-	if err := c.Check1Arg(); err != nil {
-		return nil, err
-	}
-	cmd, err := c.StringArg(0)
-	if err != nil {
-		return nil, err
-	}
-
-	strms := &streams{}
-	var terminalOut bool
-	if len(c.Etc()) != 0 {
-		tout := c.Etc()[0]
-
-		var ok bool
-		terminalOut, ok = tout.TryBool()
-		if !ok {
-			luastreams, ok := tout.TryTable()
-			if !ok {
-				return nil, errors.New("bad argument to run (expected boolean or table, got " + tout.TypeName() + ")")
-			}
-
-			handleStream(luastreams.Get(rt.StringValue("out")), strms, false)
-			handleStream(luastreams.Get(rt.StringValue("err")), strms, true)
-
-			stdinstrm := luastreams.Get(rt.StringValue("input"))
-			if !stdinstrm.IsNil() {
-				ud, ok := stdinstrm.TryUserData()
-				if !ok {
-					return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file, got " + stdinstrm.TypeName() + ")")
-				}
-
-				val := ud.Value()
-				var varstrm io.Reader
-				if f, ok := val.(*iolib.File); ok {
-					varstrm = f.Handle()
-				}
-
-				if f, ok := val.(*sink); ok {
-					varstrm = f.reader
-				}
-
-				if varstrm == nil {
-					return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file)")
-				}
-
-				strms.stdin = varstrm
-			}
-		} else {
-			if !terminalOut {
-				strms = &streams{
-					stdout: new(bytes.Buffer),
-					stderr: new(bytes.Buffer),
-				}
-			}
-		}
-	}
-
-	var exitcode uint8
-	stdout, stderr, err := execCommand(cmd, strms)
-
-	if code, ok := interp.IsExitStatus(err); ok {
-		exitcode = code
-	} else if err != nil {
-		exitcode = 1
-	}
-
-	var stdoutStr, stderrStr string
-	if stdoutBuf, ok := stdout.(*bytes.Buffer); ok {
-		stdoutStr = stdoutBuf.String()
-	}
-	if stderrBuf, ok := stderr.(*bytes.Buffer); ok {
-		stderrStr = stderrBuf.String()
-	}
-
-	return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil
-}
-*/
-
 // cwd() -> string
 // Returns the current directory of the shell.
 // #returns string
diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go
index 61ca254..a8abf0a 100644
--- a/golibs/snail/lua.go
+++ b/golibs/snail/lua.go
@@ -1,13 +1,17 @@
 package snail
 
 import (
+	"errors"
 	"fmt"
+	"io"
 	"strings"
 
+	"hilbish/sink"
 	"hilbish/util"
 
 	rt "github.com/arnodel/golua/runtime"
 	"github.com/arnodel/golua/lib/packagelib"
+	"github.com/arnodel/golua/lib/iolib"
 	"mvdan.cc/sh/v3/interp"
 	"mvdan.cc/sh/v3/syntax"
 )
@@ -22,7 +26,7 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
 	snailMeta := rt.NewTable()
 	snailMethods := rt.NewTable()
 	snailFuncs := map[string]util.LuaExport{
-		"run": {srun, 2, false},
+		"run": {srun, 3, false},
 	}
 	util.SetExports(rtm, snailMethods, snailFuncs)
 
@@ -65,11 +69,27 @@ func srun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
+	streams := &util.Streams{}
+	thirdArg := c.Arg(2)
+	switch thirdArg.Type() {
+		case rt.TableType:
+			args := thirdArg.AsTable()
+
+			if luastreams, ok := args.Get(rt.StringValue("sinks")).TryTable(); ok {
+				handleStream(luastreams.Get(rt.StringValue("out")), streams, false, false)
+				handleStream(luastreams.Get(rt.StringValue("err")), streams, true, false)
+				handleStream(luastreams.Get(rt.StringValue("input")), streams, false, true)
+			}
+		case rt.NilType: // noop
+		default:
+			return nil, errors.New("expected 3rd arg to either be a table or a boolean")
+	}
+
 	var newline bool
 	var cont bool
 	var luaErr rt.Value = rt.NilValue
 	exitCode := 0
-	bg, _, _, err := s.Run(cmd, nil)
+	bg, _, _, err := s.Run(cmd, streams)
 	if err != nil {
 		if syntax.IsIncomplete(err) {
 			/*
@@ -103,6 +123,41 @@ func srun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	return c.PushingNext1(t.Runtime, rt.TableValue(runnerRet)), nil
 }
 
+func handleStream(v rt.Value, strms *util.Streams, errStream, inStream bool) error {
+	if v == rt.NilValue {
+		return nil
+	}
+
+	ud, ok := v.TryUserData()
+	if !ok {
+		return errors.New("expected metatable argument")
+	}
+
+	val := ud.Value()
+	var varstrm io.ReadWriter
+	if f, ok := val.(*iolib.File); ok {
+		varstrm = f.Handle()
+	}
+
+	if f, ok := val.(*sink.Sink); ok {
+		varstrm = f.Rw
+	}
+
+	if varstrm == nil {
+		return errors.New("expected either a sink or file")
+	}
+
+	if errStream {
+		strms.Stderr = varstrm
+	} else if inStream {
+		strms.Stdin = varstrm
+	} else {
+		strms.Stdout = varstrm
+	}
+
+	return nil
+}
+
 func snailArg(c *rt.GoCont, arg int) (*snail, error) {
 	s, ok := valueToSnail(c.Arg(arg))
 	if !ok {
diff --git a/nature/hilbish.lua b/nature/hilbish.lua
new file mode 100644
index 0000000..f37bab9
--- /dev/null
+++ b/nature/hilbish.lua
@@ -0,0 +1,30 @@
+local hilbish = require 'hilbish'
+local snail = require 'snail'
+
+hilbish.snail = snail.new()
+
+function hilbish.run(cmd, streams)
+	local sinks = {}
+
+	if type(streams) == 'boolean' then
+		if not streams then
+			sinks = {
+				out = hilbish.sink.new(),
+				err = hilbish.sink.new(),
+				input = io.stdin
+			}
+		end
+	elseif type(streams) == 'table' then
+		sinks = streams
+	end
+
+	local out = hilbish.snail:run(cmd, {sinks = sinks})
+	local returns = {out}
+
+	if type(streams) == 'boolean' and not streams then
+		table.insert(returns, sinks.out:readAll())
+		table.insert(returns, sinks.err:readAll())
+	end
+
+	return table.unpack(returns)
+end
diff --git a/nature/init.lua b/nature/init.lua
index a0579d7..4c47bfe 100644
--- a/nature/init.lua
+++ b/nature/init.lua
@@ -18,6 +18,8 @@ table.insert(package.searchers, function(module)
 	return function() return hilbish.module.load(path) end, path
 end)
 
+require 'nature.hilbish'
+
 require 'nature.commands'
 require 'nature.completions'
 require 'nature.opts'
diff --git a/nature/runner.lua b/nature/runner.lua
index 9ece224..6bb0b22 100644
--- a/nature/runner.lua
+++ b/nature/runner.lua
@@ -83,9 +83,8 @@ function hilbish.runner.getCurrent()
 	return currentRunner
 end
 
-local snaili = snail.new()
 function hilbish.runner.sh(input)
-	return snaili:run(input)
+	return hilbish.snail:run(input)
 end
 
 hilbish.runner.add('hybrid', function(input)
diff --git a/sink/sink.go b/sink/sink.go
index 4899d89..2b17373 100644
--- a/sink/sink.go
+++ b/sink/sink.go
@@ -19,7 +19,7 @@ var sinkMetaKey = rt.StringValue("hshsink")
 // A sink is a structure that has input and/or output to/from
 // a desination.
 type Sink struct{
-	rw *bufio.ReadWriter
+	Rw *bufio.ReadWriter
 	file *os.File
 	UserData *rt.UserData
 	autoFlush bool
@@ -99,7 +99,7 @@ func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 
 	lines := []string{}
 	for {
-		line, err := s.rw.ReadString('\n')
+		line, err := s.Rw.ReadString('\n')
 		if err != nil {
 			if err == io.EOF {
 				break
@@ -128,7 +128,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	str, _ := s.rw.ReadString('\n')
+	str, _ := s.Rw.ReadString('\n')
 
 	return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil
 }
@@ -150,9 +150,9 @@ func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	s.rw.Write([]byte(data))
+	s.Rw.Write([]byte(data))
 	if s.autoFlush {
-		s.rw.Flush()
+		s.Rw.Flush()
 	}
 
 	return c.Next(), nil
@@ -175,9 +175,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	s.rw.Write([]byte(data + "\n"))
+	s.Rw.Write([]byte(data + "\n"))
 	if s.autoFlush {
-		s.rw.Flush()
+		s.Rw.Flush()
 	}
 
 	return c.Next(), nil
@@ -196,7 +196,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 		return nil, err
 	}
 
-	s.rw.Flush()
+	s.Rw.Flush()
 
 	return c.Next(), nil
 }
@@ -227,13 +227,13 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
 	return c.Next(), nil
 }
 
-func NewSink(rtm *rt.Runtime, rw io.ReadWriter) *Sink {
+func NewSink(rtm *rt.Runtime, Rw io.ReadWriter) *Sink {
 	s := &Sink{
-		rw: bufio.NewReadWriter(bufio.NewReader(rw), bufio.NewWriter(rw)),
+		Rw: bufio.NewReadWriter(bufio.NewReader(Rw), bufio.NewWriter(Rw)),
 	}
 	s.UserData = sinkUserData(rtm, s)
 
-	if f, ok := rw.(*os.File); ok {
+	if f, ok := Rw.(*os.File); ok {
 		s.file = f
 	}
 
@@ -242,7 +242,7 @@ func NewSink(rtm *rt.Runtime, rw io.ReadWriter) *Sink {
 
 func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
 	s := &Sink{
-		rw: bufio.NewReadWriter(bufio.NewReader(r), nil),
+		Rw: bufio.NewReadWriter(bufio.NewReader(r), nil),
 	}
 	s.UserData = sinkUserData(rtm, s)
 
@@ -255,7 +255,7 @@ func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink {
 
 func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink {
 	s := &Sink{
-		rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)),
+		Rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)),
 		autoFlush: true,
 	}
 	s.UserData = sinkUserData(rtm, s)