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

feat: transition to writing readline lua functions in readline package, and some misc stuff

This commit is contained in:
sammyette 2025-06-12 23:24:42 -04:00
parent 2117a3747b
commit 45ceb9097f
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
7 changed files with 212 additions and 59 deletions

85
api.go
View File

@ -25,30 +25,31 @@ import (
"hilbish/util" "hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib" "github.com/arnodel/golua/lib/packagelib"
rt "github.com/arnodel/golua/runtime"
//"github.com/arnodel/golua/lib/iolib" //"github.com/arnodel/golua/lib/iolib"
"github.com/maxlandon/readline" "github.com/maxlandon/readline"
//"mvdan.cc/sh/v3/interp" //"mvdan.cc/sh/v3/interp"
) )
var exports = map[string]util.LuaExport{ var exports = map[string]util.LuaExport{
"alias": {hlalias, 2, false}, "alias": {hlalias, 2, false},
"appendPath": {hlappendPath, 1, false}, "appendPath": {hlappendPath, 1, false},
"complete": {hlcomplete, 2, false}, "complete": {hlcomplete, 2, false},
"cwd": {hlcwd, 0, false}, "cwd": {hlcwd, 0, false},
"exec": {hlexec, 1, false}, "exec": {hlexec, 1, false},
"goro": {hlgoro, 1, true}, "goro": {hlgoro, 1, true},
"highlighter": {hlhighlighter, 1, false}, "highlighter": {hlhighlighter, 1, false},
"hinter": {hlhinter, 1, false}, "hinter": {hlhinter, 1, false},
"multiprompt": {hlmultiprompt, 1, false}, "multiprompt": {hlmultiprompt, 1, false},
"prependPath": {hlprependPath, 1, false}, "prependPath": {hlprependPath, 1, false},
"prompt": {hlprompt, 1, true}, "prompt": {hlprompt, 1, true},
"inputMode": {hlinputMode, 1, false}, "inputMode": {hlinputMode, 1, false},
"interval": {hlinterval, 2, false}, "interval": {hlinterval, 2, false},
"read": {hlread, 1, false}, "read": {hlread, 1, false},
"timeout": {hltimeout, 2, false}, "timeout": {hltimeout, 2, false},
"which": {hlwhich, 1, false}, "which": {hlwhich, 1, false},
} }
var hshMod *rt.Table var hshMod *rt.Table
@ -118,9 +119,6 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
timersModule := timers.loader(rtm) timersModule := timers.loader(rtm)
mod.Set(rt.StringValue("timers"), rt.TableValue(timersModule)) mod.Set(rt.StringValue("timers"), rt.TableValue(timersModule))
editorModule := editorLoader(rtm)
mod.Set(rt.StringValue("editor"), rt.TableValue(editorModule))
versionModule := rt.NewTable() versionModule := rt.NewTable()
util.SetField(rtm, versionModule, "branch", rt.StringValue(gitBranch)) util.SetField(rtm, versionModule, "branch", rt.StringValue(gitBranch))
util.SetField(rtm, versionModule, "full", rt.StringValue(getVersion())) util.SetField(rtm, versionModule, "full", rt.StringValue(getVersion()))
@ -138,11 +136,11 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
} }
func getenv(key, fallback string) string { func getenv(key, fallback string) string {
value := os.Getenv(key) value := os.Getenv(key)
if len(value) == 0 { if len(value) == 0 {
return fallback return fallback
} }
return value return value
} }
func setVimMode(mode string) { func setVimMode(mode string) {
@ -194,7 +192,6 @@ func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil
} }
// read(prompt) -> input (string) // read(prompt) -> input (string)
// Read input from the user, using Hilbish's line editor/input reader. // Read input from the user, using Hilbish's line editor/input reader.
// This is a separate instance from the one Hilbish actually uses. // This is a separate instance from the one Hilbish actually uses.
@ -212,7 +209,7 @@ func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// substitute with an empty string // substitute with an empty string
prompt = "" prompt = ""
} }
lualr := &lineReader{ lualr := &lineReader{
rl: readline.NewInstance(), rl: readline.NewInstance(),
} }
@ -265,11 +262,13 @@ func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
} }
switch typ { switch typ {
case "left": case "left":
prompt = p prompt = p
lr.SetPrompt(fmtPrompt(prompt)) lr.SetPrompt(fmtPrompt(prompt))
case "right": lr.SetRightPrompt(fmtPrompt(p)) case "right":
default: return nil, errors.New("expected prompt type to be right or left, got " + typ) lr.SetRightPrompt(fmtPrompt(p))
default:
return nil, errors.New("expected prompt type to be right or left, got " + typ)
} }
return c.Next(), nil return c.Next(), nil
@ -290,7 +289,7 @@ will look like:
user ~ echo "hey user ~ echo "hey
--> ...!" --> ...!"
so then you get so then you get
user ~ echo "hey user ~ echo "hey
--> ...!" --> ...!"
hey ...! hey ...!
@ -386,7 +385,7 @@ func appendPath(dir string) {
// if dir isnt already in $PATH, add it // if dir isnt already in $PATH, add it
if !strings.Contains(pathenv, dir) { if !strings.Contains(pathenv, dir) {
os.Setenv("PATH", pathenv + string(os.PathListSeparator) + dir) os.Setenv("PATH", pathenv+string(os.PathListSeparator)+dir)
} }
} }
@ -480,7 +479,7 @@ func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
interval := time.Duration(ms) * time.Millisecond interval := time.Duration(ms) * time.Millisecond
timer := timers.create(timerTimeout, interval, cb) timer := timers.create(timerTimeout, interval, cb)
timer.start() timer.start()
return c.PushingNext1(t.Runtime, rt.UserDataValue(timer.ud)), nil return c.PushingNext1(t.Runtime, rt.UserDataValue(timer.ud)), nil
} }
@ -571,7 +570,7 @@ func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// if dir isnt already in $PATH, add in // if dir isnt already in $PATH, add in
if !strings.Contains(pathenv, dir) { if !strings.Contains(pathenv, dir) {
os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv) os.Setenv("PATH", dir+string(os.PathListSeparator)+pathenv)
} }
return c.Next(), nil return c.Next(), nil
@ -625,14 +624,14 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
} }
switch mode { switch mode {
case "emacs": case "emacs":
unsetVimMode() unsetVimMode()
lr.rl.InputMode = readline.Emacs lr.rl.InputMode = readline.Emacs
case "vim": case "vim":
setVimMode("insert") setVimMode("insert")
lr.rl.InputMode = readline.Vim lr.rl.InputMode = readline.Vim
default: default:
return nil, errors.New("inputMode: expected vim or emacs, received " + mode) return nil, errors.New("inputMode: expected vim or emacs, received " + mode)
} }
return c.Next(), nil return c.Next(), nil
@ -667,7 +666,9 @@ func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// #example // #example
// --This code will highlight all double quoted strings in green. // --This code will highlight all double quoted strings in green.
// function hilbish.highlighter(line) // function hilbish.highlighter(line)
// return line:gsub('"%w+"', function(c) return lunacolors.green(c) end) //
// return line:gsub('"%w+"', function(c) return lunacolors.green(c) end)
//
// end // end
// #example // #example
// #param line string // #param line string

2
go.mod
View File

@ -30,7 +30,7 @@ require (
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73 replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73
replace github.com/maxlandon/readline => ./readline replace github.com/maxlandon/readline => ./golibs/readline
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10 replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"regexp" "regexp"
"sync" "sync"
"github.com/arnodel/golua/lib/packagelib"
) )
// Instance is used to encapsulate the parameter group and run time of any given // Instance is used to encapsulate the parameter group and run time of any given
@ -31,13 +33,13 @@ type Instance struct {
Multiline bool // If set to true, the shell will have a two-line prompt. Multiline bool // If set to true, the shell will have a two-line prompt.
MultilinePrompt string // If multiline is true, this is the content of the 2nd line. MultilinePrompt string // If multiline is true, this is the content of the 2nd line.
mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt
rightPrompt string rightPrompt string
rightPromptLen int rightPromptLen int
realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line. realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line.
defaultPrompt []rune defaultPrompt []rune
promptLen int promptLen int
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs
// //
// Input Line --------------------------------------------------------------------------------- // Input Line ---------------------------------------------------------------------------------
@ -114,9 +116,9 @@ type Instance struct {
searchMode FindMode // Used for varying hints, and underlying functions called searchMode FindMode // Used for varying hints, and underlying functions called
regexSearch *regexp.Regexp // Holds the current search regex match regexSearch *regexp.Regexp // Holds the current search regex match
search string search string
mainHist bool // Which history stdin do we want mainHist bool // Which history stdin do we want
histInfo []rune // We store a piece of hist info, for dual history sources histInfo []rune // We store a piece of hist info, for dual history sources
Searcher func(string, []string) []string Searcher func(string, []string) []string
// //
// History ----------------------------------------------------------------------------------- // History -----------------------------------------------------------------------------------
@ -200,12 +202,14 @@ type Instance struct {
// concurency // concurency
mutex sync.Mutex mutex sync.Mutex
ViModeCallback func(ViMode) ViModeCallback func(ViMode)
ViActionCallback func(ViAction, []string) ViActionCallback func(ViAction, []string)
RawInputCallback func([]rune) // called on all input RawInputCallback func([]rune) // called on all input
bufferedOut *bufio.Writer bufferedOut *bufio.Writer
Loader packagelib.Loader
} }
// NewInstance is used to create a readline instance and initialise it with sane defaults. // NewInstance is used to create a readline instance and initialise it with sane defaults.
@ -245,7 +249,9 @@ func NewInstance() *Instance {
} }
for _, hay := range haystack { for _, hay := range haystack {
if rl.regexSearch == nil { continue } if rl.regexSearch == nil {
continue
}
if rl.regexSearch.MatchString(hay) { if rl.regexSearch.MatchString(hay) {
suggs = append(suggs, hay) suggs = append(suggs, hay)
} }
@ -256,6 +262,11 @@ func NewInstance() *Instance {
rl.bufferedOut = bufio.NewWriter(os.Stdout) rl.bufferedOut = bufio.NewWriter(os.Stdout)
rl.Loader = packagelib.Loader{
Name: "readline",
Load: rl.luaLoader,
}
// Registers // Registers
rl.initRegisters() rl.initRegisters()

124
golibs/readline/lua.go Normal file
View File

@ -0,0 +1,124 @@
// line reader library
// The readline module is responsible for reading input from the user.
// The readline module is what Hilbish uses to read input from the user,
// including all the interactive features of Hilbish like history search,
// syntax highlighting, everything. The global Hilbish readline instance
// is usable at `hilbish.editor`.
package readline
import (
"fmt"
"io"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var rlMetaKey = rt.StringValue("__readline")
func (rl *Instance) luaLoader(rtm *rt.Runtime) (rt.Value, func()) {
rlMethods := rt.NewTable()
rlMethodss := map[string]util.LuaExport{
"read": {luaRead, 1, false},
"insert": {luaInsert, 2, false},
}
util.SetExports(rtm, rlMethods, rlMethodss)
jobMeta := rt.NewTable()
rlIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
_, err := rlArg(c, 0)
if err != nil {
return nil, err
}
arg := c.Arg(1)
val := rlMethods.Get(arg)
return c.PushingNext1(t.Runtime, val), nil
}
jobMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(rlIndex, "__index", 2, false)))
rtm.SetRegistry(rlMetaKey, rt.TableValue(jobMeta))
rlFuncs := map[string]util.LuaExport{
"new": {luaNew, 0, false},
}
luaRl := rt.NewTable()
util.SetExports(rtm, luaRl, rlFuncs)
return rt.TableValue(luaRl), nil
}
func luaNew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
rl := NewInstance()
ud := rlUserData(t.Runtime, rl)
return c.PushingNext1(t.Runtime, rt.UserDataValue(ud)), nil
}
func luaInsert(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
}
text, err := c.StringArg(1)
if err != nil {
return nil, err
}
rl.insert([]rune(text))
return c.Next(), nil
}
func luaRead(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
}
inp, err := rl.Readline()
if err == EOF {
fmt.Println("")
return nil, io.EOF
} else if err != nil {
return nil, err
}
return c.PushingNext1(t.Runtime, rt.StringValue(inp)), nil
}
func rlArg(c *rt.GoCont, arg int) (*Instance, error) {
j, ok := valueToRl(c.Arg(arg))
if !ok {
return nil, fmt.Errorf("#%d must be a readline", arg+1)
}
return j, nil
}
func valueToRl(val rt.Value) (*Instance, bool) {
u, ok := val.TryUserData()
if !ok {
return nil, false
}
j, ok := u.Value().(*Instance)
return j, ok
}
func rlUserData(rtm *rt.Runtime, rl *Instance) *rt.UserData {
rlMeta := rtm.Registry(rlMetaKey)
return rt.NewUserData(rl, rlMeta.AsTable())
}

8
lua.go
View File

@ -5,16 +5,16 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"hilbish/util"
"hilbish/golibs/bait" "hilbish/golibs/bait"
"hilbish/golibs/commander" "hilbish/golibs/commander"
"hilbish/golibs/fs" "hilbish/golibs/fs"
"hilbish/golibs/snail" "hilbish/golibs/snail"
"hilbish/golibs/terminal" "hilbish/golibs/terminal"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib" "github.com/arnodel/golua/lib"
"github.com/arnodel/golua/lib/debuglib" "github.com/arnodel/golua/lib/debuglib"
rt "github.com/arnodel/golua/runtime"
) )
var minimalconf = `hilbish.prompt '& '` var minimalconf = `hilbish.prompt '& '`
@ -55,10 +55,10 @@ func luaInit() {
cmds = commander.New(l) cmds = commander.New(l)
lib.LoadLibs(l, cmds.Loader) lib.LoadLibs(l, cmds.Loader)
lib.LoadLibs(l, lr.rl.Loader)
// Add more paths that Lua can require from // Add more paths that Lua can require from
_, err := util.DoString(l, "package.path = package.path .. " + requirePaths) _, err := util.DoString(l, "package.path = package.path .. "+requirePaths)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.") fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.")
} }

3
nature/editor.lua Normal file
View File

@ -0,0 +1,3 @@
local readline = require 'readline'
hilbish.editor = readline.new()

View File

@ -28,6 +28,7 @@ require 'nature.vim'
require 'nature.runner' require 'nature.runner'
require 'nature.hummingbird' require 'nature.hummingbird'
require 'nature.abbr' require 'nature.abbr'
require 'nature.editor'
local shlvl = tonumber(os.getenv 'SHLVL') local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then if shlvl ~= nil then
@ -94,3 +95,16 @@ end)
bait.catch('command.not-executable', function(cmd) bait.catch('command.not-executable', function(cmd)
print(string.format('hilbish: %s: not executable', cmd)) print(string.format('hilbish: %s: not executable', cmd))
end) end)
print 'we are at the end'
while true do
print 'reading input'
local ok, res = pcall(function() return hilbish.editor:read() end)
if not ok then
print(res)
print(res == 'EOF')
os.exit(0)
end
print(ok, res)
end