2
2
mirror of https://github.com/Hilbis/Hilbish synced 2025-07-01 16:52:03 +00:00

refactor: rewrite parts of hilbish in lua

exec code, config running code is now written in lua.
This commit is contained in:
sammyette 2025-06-15 18:02:26 -04:00
parent 49f2bae9e1
commit e87136de7a
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
10 changed files with 303 additions and 237 deletions

89
api.go
View File

@ -6,10 +6,13 @@
// #field user Username of the user
// #field host Hostname of the machine
// #field dataDir Directory for Hilbish data files, including the docs and default modules
// #field defaultConfDir Default directory Hilbish runs its config file from
// #field confFile File to run as Hilbish config, this is only set with the -C flag
// #field interactive Is Hilbish in an interactive shell?
// #field login Is Hilbish the login shell?
// #field vimMode Current Vim input mode of Hilbish (will be nil if not in Vim input mode)
// #field exitCode Exit code of the last executed command
// #field exitCode If Hilbish is currently running any interactive input
package main
import (
@ -44,10 +47,8 @@ var exports = map[string]util.LuaExport{
"hinter": {hlhinter, 1, false},
"multiprompt": {hlmultiprompt, 1, false},
"prependPath": {hlprependPath, 1, false},
"prompt": {hlprompt, 1, true},
"inputMode": {hlinputMode, 1, false},
"interval": {hlinterval, 2, false},
"read": {hlread, 1, false},
"timeout": {hltimeout, 2, false},
"which": {hlwhich, 1, false},
}
@ -79,6 +80,8 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
util.SetField(rtm, mod, "host", rt.StringValue(host))
util.SetField(rtm, mod, "home", rt.StringValue(curuser.HomeDir))
util.SetField(rtm, mod, "dataDir", rt.StringValue(dataDir))
util.SetField(rtm, mod, "defaultConfDir", rt.StringValue(defaultConfDir))
util.SetField(rtm, mod, "confFile", rt.StringValue(confPath))
util.SetField(rtm, mod, "interactive", rt.BoolValue(interactive))
util.SetField(rtm, mod, "login", rt.BoolValue(login))
util.SetField(rtm, mod, "vimMode", rt.NilValue)
@ -194,88 +197,6 @@ func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil
}
// read(prompt) -> input (string)
// Read input from the user, using Hilbish's line editor/input reader.
// This is a separate instance from the one Hilbish actually uses.
// Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
// #param prompt? string Text to print before input, can be empty.
// #returns string|nil
func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
luaprompt := c.Arg(0)
if typ := luaprompt.Type(); typ != rt.StringType && typ != rt.NilType {
return nil, errors.New("expected #1 to be a string")
}
prompt, ok := luaprompt.TryString()
if !ok {
// if we are here and `luaprompt` is not a string, it's nil
// substitute with an empty string
prompt = ""
}
lualr := &lineReader{
rl: readline.NewInstance(),
}
lualr.SetPrompt(prompt)
input, err := lualr.Read()
if err != nil {
return c.Next(), nil
}
return c.PushingNext1(t.Runtime, rt.StringValue(input)), nil
}
/*
prompt(str, typ)
Changes the shell prompt to the provided string.
There are a few verbs that can be used in the prompt text.
These will be formatted and replaced with the appropriate values.
`%d` - Current working directory
`%u` - Name of current user
`%h` - Hostname of device
#param str string
#param typ? string Type of prompt, being left or right. Left by default.
#example
-- the default hilbish prompt without color
hilbish.prompt '%u %d '
-- or something of old:
hilbish.prompt '%u@%h :%d $'
-- prompt: user@hostname: ~/directory $
#example
*/
func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := c.Check1Arg()
if err != nil {
return nil, err
}
p, err := c.StringArg(0)
if err != nil {
return nil, err
}
typ := "left"
// optional 2nd arg
if len(c.Etc()) != 0 {
ltyp := c.Etc()[0]
var ok bool
typ, ok = ltyp.TryString()
if !ok {
return nil, errors.New("bad argument to run (expected string, got " + ltyp.TypeName() + ")")
}
}
switch typ {
case "left":
prompt = p
lr.SetPrompt(fmtPrompt(prompt))
case "right":
lr.SetRightPrompt(fmtPrompt(p))
default:
return nil, errors.New("expected prompt type to be right or left, got " + typ)
}
return c.Next(), nil
}
// multiprompt(str)
// Changes the text prompt when Hilbish asks for more input.
// This will show up when text is incomplete, like a missing quote

View File

@ -1,7 +1,6 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
@ -10,10 +9,6 @@ import (
//"github.com/yuin/gopher-lua/parse"
)
var errNotExec = errors.New("not executable")
var errNotFound = errors.New("not found")
var runnerMode rt.Value = rt.NilValue
func runInput(input string, priv bool) {
running = true
runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run"))

View File

@ -9,6 +9,7 @@ package readline
import (
"fmt"
"io"
"strings"
"hilbish/util"
@ -28,6 +29,8 @@ func (rl *Readline) luaLoader(rtm *rt.Runtime) (rt.Value, func()) {
"readChar": {rlReadChar, 1, false},
"setVimRegister": {rlSetRegister, 3, false},
"log": {rlLog, 2, false},
"prompt": {rlPrompt, 2, false},
"refreshPrompt": {rlRefreshPrompt, 1, false},
}
util.SetExports(rtm, rlMethods, rlMethodss)
@ -251,6 +254,53 @@ func rlLog(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.Next(), nil
}
// #member
// prompt(text)
// Sets the prompt of the line reader. This is the text that shows up before user input.
func rlPrompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
p, err := c.StringArg(1)
if err != nil {
return nil, err
}
fmt.Println(p)
halfPrompt := strings.Split(p, "\n")
if len(halfPrompt) > 1 {
rl.Multiline = true
rl.SetPrompt(strings.Join(halfPrompt[:len(halfPrompt)-1], "\n"))
rl.MultilinePrompt = halfPrompt[len(halfPrompt)-1:][0]
} else {
rl.Multiline = false
rl.MultilinePrompt = ""
rl.SetPrompt(p)
}
return c.Next(), nil
}
func rlRefreshPrompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
rl.RefreshPromptInPlace("")
return c.Next(), nil
}
func rlArg(c *rt.GoCont, arg int) (*Readline, error) {
j, ok := valueToRl(c.Arg(arg))
if !ok {

21
lua.go
View File

@ -16,14 +16,19 @@ import (
"github.com/arnodel/golua/lib"
"github.com/arnodel/golua/lib/debuglib"
rt "github.com/arnodel/golua/runtime"
"github.com/pborman/getopt"
)
var minimalconf = `hilbish.prompt '& '`
func luaInit() {
l = rt.New(os.Stdout)
loadLibs(l)
luaArgs := rt.NewTable()
for i, arg := range getopt.Args() {
luaArgs.Set(rt.IntValue(int64(i)), rt.StringValue(arg))
}
l.GlobalEnv().Set(rt.StringValue("args"), rt.TableValue(luaArgs))
yarnPool := yarn.New(yarnloadLibs)
lib.LoadLibs(l, yarnPool.Loader)
@ -36,6 +41,7 @@ func luaInit() {
err1 := util.DoFile(l, "nature/init.lua")
if err1 != nil {
fmt.Println(err1)
err2 := util.DoFile(l, filepath.Join(dataDir, "nature", "init.lua"))
if err2 != nil {
fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.")
@ -98,14 +104,3 @@ func yarnloadLibs(r *rt.Runtime) {
lib.LoadLibs(l, lr.rl.Loader)
}
func runConfig(confpath string) {
if !interactive {
return
}
err := util.DoFile(l, confpath)
if err != nil {
fmt.Fprintln(os.Stderr, err, "\nAn error has occured while loading your config! Falling back to minimal default config.")
util.DoString(l, minimalconf)
}
}

175
main.go
View File

@ -2,40 +2,36 @@ package main
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"syscall"
"hilbish/util"
"hilbish/golibs/bait"
"hilbish/golibs/commander"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/pborman/getopt"
"github.com/maxlandon/readline"
"golang.org/x/term"
)
var (
l *rt.Runtime
l *rt.Runtime
lr *lineReader
luaCompletions = map[string]*rt.Closure{}
confDir string
confDir string
userDataDir string
curuser *user.User
curuser *user.User
hooks *bait.Bait
cmds *commander.Commander
defaultConfPath string
hooks *bait.Bait
cmds *commander.Commander
confPath string
defaultHistPath string
)
@ -62,7 +58,7 @@ func main() {
// i honestly dont know what directories to use for this
switch runtime.GOOS {
case "linux", "darwin":
userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir + "/.local/share")
userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir+"/.local/share")
default:
// this is fine on windows, dont know about others
userDataDir = confDir
@ -75,7 +71,7 @@ func main() {
// else do ~ substitution
defaultConfDir = filepath.Join(util.ExpandHome(defaultConfDir), "hilbish")
}
defaultConfPath = filepath.Join(defaultConfDir, "init.lua")
defaultConfPath := filepath.Join(defaultConfDir, "init.lua")
if defaultHistDir == "" {
defaultHistDir = filepath.Join(userDataDir, "hilbish")
} else {
@ -95,6 +91,7 @@ func main() {
loginshflag := getopt.Lookup('l').Seen()
interactiveflag := getopt.Lookup('i').Seen()
noexecflag := getopt.Lookup('n').Seen()
confPath = *configflag
if *helpflag {
getopt.PrintUsage(os.Stdout)
@ -105,7 +102,7 @@ func main() {
interactive = true
}
if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 || !term.IsTerminal(int(os.Stdin.Fd())) {
if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode()&os.ModeCharDevice) == 0 || !term.IsTerminal(int(os.Stdin.Fd())) {
interactive = false
}
@ -143,29 +140,6 @@ func main() {
go handleSignals()
// If user's config doesn't exixt,
if _, err := os.Stat(defaultConfPath); os.IsNotExist(err) && *configflag == defaultConfPath {
// Read default from current directory
// (this is assuming the current dir is Hilbish's git)
_, err := os.ReadFile(".hilbishrc.lua")
confpath := ".hilbishrc.lua"
if err != nil {
// If it wasnt found, go to the real sample conf
sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua")
_, err = os.ReadFile(sampleConfigPath)
confpath = sampleConfigPath
if err != nil {
fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath)
return
}
}
runConfig(confpath)
} else {
runConfig(*configflag)
}
hooks.Emit("hilbish.init")
if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 {
scanner := bufio.NewScanner(bufio.NewReader(os.Stdin))
for scanner.Scan() {
@ -180,12 +154,6 @@ func main() {
}
if getopt.NArgs() > 0 {
luaArgs := rt.NewTable()
for i, arg := range getopt.Args() {
luaArgs.Set(rt.IntValue(int64(i)), rt.StringValue(arg))
}
l.GlobalEnv().Set(rt.StringValue("args"), rt.TableValue(luaArgs))
err := util.DoFile(l, getopt.Arg(0))
if err != nil {
fmt.Fprintln(os.Stderr, err)
@ -195,71 +163,73 @@ func main() {
}
initialized = true
input:
for interactive {
running = false
/*
input:
for interactive {
running = false
input, err := lr.Read()
input, err := lr.Read()
if err == io.EOF {
// Exit if user presses ^D (ctrl + d)
hooks.Emit("hilbish.exit")
break
}
if err != nil {
if err == readline.CtrlC {
fmt.Println("^C")
hooks.Emit("hilbish.cancel")
} else {
// If we get a completely random error, print
fmt.Fprintln(os.Stderr, err)
if errors.Is(err, syscall.ENOTTY) {
// what are we even doing here?
panic("not a tty")
}
<-make(chan struct{})
}
continue
}
var priv bool
if strings.HasPrefix(input, " ") {
priv = true
}
input = strings.TrimSpace(input)
if len(input) == 0 {
running = true
hooks.Emit("command.exit", 0)
continue
}
if strings.HasSuffix(input, "\\") {
print("\n")
for {
input, err = continuePrompt(strings.TrimSuffix(input, "\\") + "\n", false)
if err != nil {
running = true
lr.SetPrompt(fmtPrompt(prompt))
goto input // continue inside nested loop
}
if !strings.HasSuffix(input, "\\") {
if err == io.EOF {
// Exit if user presses ^D (ctrl + d)
hooks.Emit("hilbish.exit")
break
}
if err != nil {
if err == readline.CtrlC {
fmt.Println("^C")
hooks.Emit("hilbish.cancel")
} else {
// If we get a completely random error, print
fmt.Fprintln(os.Stderr, err)
if errors.Is(err, syscall.ENOTTY) {
// what are we even doing here?
panic("not a tty")
}
<-make(chan struct{})
}
continue
}
var priv bool
if strings.HasPrefix(input, " ") {
priv = true
}
input = strings.TrimSpace(input)
if len(input) == 0 {
running = true
hooks.Emit("command.exit", 0)
continue
}
if strings.HasSuffix(input, "\\") {
print("\n")
for {
input, err = continuePrompt(strings.TrimSuffix(input, "\\")+"\n", false)
if err != nil {
running = true
lr.SetPrompt(fmtPrompt(prompt))
goto input // continue inside nested loop
}
if !strings.HasSuffix(input, "\\") {
break
}
}
}
runInput(input, priv)
termwidth, _, err := term.GetSize(0)
if err != nil {
continue
}
fmt.Printf("\u001b[7m∆\u001b[0m" + strings.Repeat(" ", termwidth-1) + "\r")
}
}
runInput(input, priv)
termwidth, _, err := term.GetSize(0)
if err != nil {
continue
}
fmt.Printf("\u001b[7m∆\u001b[0m" + strings.Repeat(" ", termwidth - 1) + "\r")
}
*/
exit(0)
}
/*
func continuePrompt(prev string, newline bool) (string, error) {
hooks.Emit("multiline", nil)
lr.SetPrompt(multilinePrompt)
@ -279,6 +249,7 @@ func continuePrompt(prev string, newline bool) (string, error) {
return prev + cont, nil
}
*/
// This semi cursed function formats our prompt (obviously)
func fmtPrompt(prompt string) string {
@ -300,7 +271,7 @@ func fmtPrompt(prompt string) string {
}
for i, v := range args {
if i % 2 == 0 {
if i%2 == 0 {
args[i] = "%" + v
}
}
@ -354,5 +325,5 @@ func getVersion() string {
}
func cut(slice []string, idx int) []string {
return append(slice[:idx], slice[idx + 1:]...)
return append(slice[:idx], slice[idx+1:]...)
}

View File

@ -21,7 +21,11 @@ function editorMt.__index(_, key)
end
return function(...)
return editor[key](editor, ...)
local args = {...}
if args[1] == hilbish.editor then
table.remove(args, 1)
end
return editor[key](editor, table.unpack(args))
end
end

View File

@ -1,5 +1,7 @@
-- @module hilbish
local bait = require 'bait'
local fs = require 'fs'
local readline = require 'readline'
local snail = require 'snail'
hilbish.snail = snail.new()
@ -7,6 +9,81 @@ hilbish.snail:run 'true' -- to "initialize" snail
bait.catch('hilbish.cd', function(path)
hilbish.snail:dir(path)
end)
local function abbrevHome(path)
if path:sub(1, hilbish.home:len()) == hilbish.home then
return fs.join('~', path:sub(hilbish.home:len() + 1))
end
end
local function fmtPrompt(p)
return p:gsub('%%(%w)', function(c)
if c == 'd' then
return abbrevHome(hilbish.cwd())
elseif c == 'u' then
return hilbish.user
elseif c == 'h' then
return hilbish.host
end
end)
end
--- prompt(str, typ)
--- Changes the shell prompt to the provided string.
--- There are a few verbs that can be used in the prompt text.
--- These will be formatted and replaced with the appropriate values.
--- `%d` - Current working directory
--- `%u` - Name of current user
--- `%h` - Hostname of device
--- #param str string
--- #param typ? string Type of prompt, being left or right. Left by default.
--- #example
--- -- the default hilbish prompt without color
--- hilbish.prompt '%u %d ∆'
--- -- or something of old:
--- hilbish.prompt '%u@%h :%d $'
--- -- prompt: user@hostname: ~/directory $
--- #example
-- @param p string
-- @param typ string Type of prompt, either left or right
function hilbish.prompt(p, typ)
if type(p) ~= 'string' then
error('expected #1 to be string, got ' .. type(p))
end
if not typ or typ == 'left' then
hilbish.editor:prompt(fmtPrompt(p))
if not hilbish.running then
hilbish.editor:refreshPrompt()
end
elseif typ == 'right' then
hilbish.editor:rightPrompt(fmtPrompt(p))
if not hilbish.running then
hilbish.editor:refreshPrompt()
end
else
error('expected prompt type to be right or left, got ' .. tostring(typ))
end
end
--- read(prompt) -> input (string)
--- Read input from the user, using Hilbish's line editor/input reader.
--- This is a separate instance from the one Hilbish actually uses.
--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
-- @param prompt? string Text to print before input, can be empty.
-- @returns string|nil
function hilbish.read(prompt)
prompt = prompt or ''
if type(prompt) ~= 'string' then
error 'expected #1 to be a string'
end
local rl = readline.new()
rl:prompt(prompt)
return rl:read()
end
--- 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.

View File

@ -18,6 +18,7 @@ table.insert(package.searchers, function(module)
return function() return hilbish.module.load(path) end, path
end)
require 'nature.editor'
require 'nature.hilbish'
require 'nature.processors'
@ -28,7 +29,6 @@ require 'nature.vim'
require 'nature.runner'
require 'nature.hummingbird'
require 'nature.abbr'
require 'nature.editor'
local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then
@ -95,3 +95,81 @@ end)
bait.catch('command.not-executable', function(cmd)
print(string.format('hilbish: %s: not executable', cmd))
end)
local function runConfig(path)
if not hilbish.interactive then return end
local _, err = pcall(dofile, path)
if err then
print(err)
print 'An error has occured while loading your config!\n'
hilbish.prompt '& '
else
bait.throw 'hilbish.init'
end
end
local _, err = pcall(fs.stat, hilbish.confFile)
if err and tostring(err):match 'no such file' and hilbish.confFile == fs.join(hilbish.defaultConfDir, 'init.lua') then
-- Run config from current directory (assuming this is Hilbish's git)
local _, err = pcall(fs.stat, '.hilbishrc.lua')
local confpath = '.hilbishrc.lua'
if err then
-- If it wasnt found go to system sample config
confpath = fs.join(hilbish.dataDir, confpath)
local _, err = pcall(fs.stat, confpath)
if err then
print('could not find .hilbishrc.lua or ' .. confpath)
return
end
end
runConfig(confpath)
else
runConfig(hilbish.confFile)
end
-- TODO: hilbish.exit function, stop jobs and timers.
local function exit(code)
os.exit(code)
end
while hilbish.interactive do
hilbish.running = false
local ok, res = pcall(function() return hilbish.editor:read() end)
if not ok and tostring(res):lower():match 'eof' then
bait.throw 'hilbish.exit'
exit(0)
end
if not ok then
if tostring(res):lower():match 'ctrl%+c' then
print '^C'
bait.throw 'hilbish.cancel'
else
error(res)
io.read()
end
goto continue
end
--- @type string
local input = res
local priv = false
if res:sub(1, 1) == ' ' then
priv = true
end
input = input:gsub('%s+', '')
if input:len() == 0 then
hilbish.running = true
bait.throw('command.exit', 0 )
goto continue
end
hilbish.running = true
hilbish.runner.run(input, priv)
::continue::
end

24
rl.go
View File

@ -3,7 +3,6 @@ package main
import (
"fmt"
"io"
"strings"
"hilbish/util"
@ -232,29 +231,6 @@ func (lr *lineReader) Read() (string, error) {
return s, err // might get another error
}
func (lr *lineReader) SetPrompt(p string) {
halfPrompt := strings.Split(p, "\n")
if len(halfPrompt) > 1 {
lr.rl.Multiline = true
lr.rl.SetPrompt(strings.Join(halfPrompt[:len(halfPrompt)-1], "\n"))
lr.rl.MultilinePrompt = halfPrompt[len(halfPrompt)-1:][0]
} else {
lr.rl.Multiline = false
lr.rl.MultilinePrompt = ""
lr.rl.SetPrompt(p)
}
if initialized && !running {
lr.rl.RefreshPromptInPlace("")
}
}
func (lr *lineReader) SetRightPrompt(p string) {
lr.rl.SetRightPrompt(p)
if initialized && !running {
lr.rl.RefreshPromptInPlace("")
}
}
func (lr *lineReader) AddHistory(cmd string) {
lr.fileHist.Write(cmd)
}

13
vars.go
View File

@ -2,16 +2,16 @@ package main
// String vars that are free to be changed at compile time
var (
defaultHistDir = ""
defaultHistDir = ""
commonRequirePaths = "';./libs/?/init.lua;./?/init.lua;./?/?.lua'"
prompt string
prompt string
multilinePrompt = "> "
)
// Version info
var (
ver = "v2.4.0"
ver = "v2.4.0"
releaseName = "Moonflower"
gitCommit string
@ -20,10 +20,9 @@ var (
// Flags
var (
running bool // Is a command currently running
running bool // Is a command currently running
interactive bool
login bool // Are we the login shell?
noexecute bool // Should we run Lua or only report syntax errors
login bool // Are we the login shell?
noexecute bool // Should we run Lua or only report syntax errors
initialized bool
)