From 45ceb9097f5c217fa40cb4725495b61738eef361 Mon Sep 17 00:00:00 2001 From: sammy-ette Date: Thu, 12 Jun 2025 23:24:42 -0400 Subject: [PATCH] feat: transition to writing readline lua functions in readline package, and some misc stuff --- api.go | 85 ++++++++++++------------ go.mod | 2 +- golibs/readline/instance.go | 35 ++++++---- golibs/readline/lua.go | 124 ++++++++++++++++++++++++++++++++++++ lua.go | 8 +-- nature/editor.lua | 3 + nature/init.lua | 14 ++++ 7 files changed, 212 insertions(+), 59 deletions(-) create mode 100644 golibs/readline/lua.go create mode 100644 nature/editor.lua diff --git a/api.go b/api.go index 315884c8..ccbbdfae 100644 --- a/api.go +++ b/api.go @@ -25,30 +25,31 @@ import ( "hilbish/util" - rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" + rt "github.com/arnodel/golua/runtime" + //"github.com/arnodel/golua/lib/iolib" "github.com/maxlandon/readline" //"mvdan.cc/sh/v3/interp" ) var exports = map[string]util.LuaExport{ - "alias": {hlalias, 2, false}, - "appendPath": {hlappendPath, 1, false}, - "complete": {hlcomplete, 2, false}, - "cwd": {hlcwd, 0, false}, - "exec": {hlexec, 1, false}, - "goro": {hlgoro, 1, true}, + "alias": {hlalias, 2, false}, + "appendPath": {hlappendPath, 1, false}, + "complete": {hlcomplete, 2, false}, + "cwd": {hlcwd, 0, false}, + "exec": {hlexec, 1, false}, + "goro": {hlgoro, 1, true}, "highlighter": {hlhighlighter, 1, false}, - "hinter": {hlhinter, 1, false}, + "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}, + "prompt": {hlprompt, 1, true}, + "inputMode": {hlinputMode, 1, false}, + "interval": {hlinterval, 2, false}, + "read": {hlread, 1, false}, + "timeout": {hltimeout, 2, false}, + "which": {hlwhich, 1, false}, } var hshMod *rt.Table @@ -118,9 +119,6 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { timersModule := timers.loader(rtm) mod.Set(rt.StringValue("timers"), rt.TableValue(timersModule)) - editorModule := editorLoader(rtm) - mod.Set(rt.StringValue("editor"), rt.TableValue(editorModule)) - versionModule := rt.NewTable() util.SetField(rtm, versionModule, "branch", rt.StringValue(gitBranch)) 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 { - value := os.Getenv(key) - if len(value) == 0 { - return fallback - } - return value + value := os.Getenv(key) + if len(value) == 0 { + return fallback + } + return value } 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 } - // 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. @@ -212,7 +209,7 @@ func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // substitute with an empty string prompt = "" } - + lualr := &lineReader{ rl: readline.NewInstance(), } @@ -265,11 +262,13 @@ func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } 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) + 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 @@ -290,7 +289,7 @@ will look like: user ~ ∆ echo "hey --> ...!" -so then you get +so then you get user ~ ∆ echo "hey --> ...!" hey ...! @@ -386,7 +385,7 @@ func appendPath(dir string) { // if dir isnt already in $PATH, add it 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 timer := timers.create(timerTimeout, interval, cb) timer.start() - + 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 !strings.Contains(pathenv, dir) { - os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv) + os.Setenv("PATH", dir+string(os.PathListSeparator)+pathenv) } return c.Next(), nil @@ -625,14 +624,14 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } switch mode { - case "emacs": - unsetVimMode() - lr.rl.InputMode = readline.Emacs - case "vim": - setVimMode("insert") - lr.rl.InputMode = readline.Vim - default: - return nil, errors.New("inputMode: expected vim or emacs, received " + mode) + case "emacs": + unsetVimMode() + lr.rl.InputMode = readline.Emacs + case "vim": + setVimMode("insert") + lr.rl.InputMode = readline.Vim + default: + return nil, errors.New("inputMode: expected vim or emacs, received " + mode) } return c.Next(), nil @@ -667,7 +666,9 @@ func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // #example // --This code will highlight all double quoted strings in green. // 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 // #example // #param line string diff --git a/go.mod b/go.mod index cc88c8ef..23bd7f69 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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 diff --git a/golibs/readline/instance.go b/golibs/readline/instance.go index 3f52bed5..0c630685 100644 --- a/golibs/readline/instance.go +++ b/golibs/readline/instance.go @@ -5,6 +5,8 @@ import ( "os" "regexp" "sync" + + "github.com/arnodel/golua/lib/packagelib" ) // 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. 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 - rightPrompt string - rightPromptLen int - realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line. - defaultPrompt []rune - promptLen int - stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs + mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt + rightPrompt string + rightPromptLen int + realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line. + defaultPrompt []rune + promptLen int + stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs // // Input Line --------------------------------------------------------------------------------- @@ -114,9 +116,9 @@ type Instance struct { searchMode FindMode // Used for varying hints, and underlying functions called regexSearch *regexp.Regexp // Holds the current search regex match search string - mainHist bool // Which history stdin do we want - histInfo []rune // We store a piece of hist info, for dual history sources - Searcher func(string, []string) []string + mainHist bool // Which history stdin do we want + histInfo []rune // We store a piece of hist info, for dual history sources + Searcher func(string, []string) []string // // History ----------------------------------------------------------------------------------- @@ -200,12 +202,14 @@ type Instance struct { // concurency mutex sync.Mutex - ViModeCallback func(ViMode) + ViModeCallback func(ViMode) ViActionCallback func(ViAction, []string) RawInputCallback func([]rune) // called on all input bufferedOut *bufio.Writer + + Loader packagelib.Loader } // 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 { - if rl.regexSearch == nil { continue } + if rl.regexSearch == nil { + continue + } if rl.regexSearch.MatchString(hay) { suggs = append(suggs, hay) } @@ -256,6 +262,11 @@ func NewInstance() *Instance { rl.bufferedOut = bufio.NewWriter(os.Stdout) + rl.Loader = packagelib.Loader{ + Name: "readline", + Load: rl.luaLoader, + } + // Registers rl.initRegisters() diff --git a/golibs/readline/lua.go b/golibs/readline/lua.go new file mode 100644 index 00000000..e206abb5 --- /dev/null +++ b/golibs/readline/lua.go @@ -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()) +} diff --git a/lua.go b/lua.go index 90ad97fb..1c407000 100644 --- a/lua.go +++ b/lua.go @@ -5,16 +5,16 @@ import ( "os" "path/filepath" - "hilbish/util" "hilbish/golibs/bait" "hilbish/golibs/commander" "hilbish/golibs/fs" "hilbish/golibs/snail" "hilbish/golibs/terminal" + "hilbish/util" - rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib" "github.com/arnodel/golua/lib/debuglib" + rt "github.com/arnodel/golua/runtime" ) var minimalconf = `hilbish.prompt '& '` @@ -55,10 +55,10 @@ func luaInit() { cmds = commander.New(l) lib.LoadLibs(l, cmds.Loader) - + lib.LoadLibs(l, lr.rl.Loader) // 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 { fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.") } diff --git a/nature/editor.lua b/nature/editor.lua new file mode 100644 index 00000000..c0596ecb --- /dev/null +++ b/nature/editor.lua @@ -0,0 +1,3 @@ +local readline = require 'readline' + +hilbish.editor = readline.new() \ No newline at end of file diff --git a/nature/init.lua b/nature/init.lua index 9b75a271..08108824 100644 --- a/nature/init.lua +++ b/nature/init.lua @@ -28,6 +28,7 @@ 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 @@ -94,3 +95,16 @@ end) bait.catch('command.not-executable', function(cmd) print(string.format('hilbish: %s: not executable', cmd)) 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