fix: update for latest changes

insensitive-tab
TorchedSammy 2022-04-20 18:18:42 -04:00
commit c6bb8bc663
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
64 changed files with 2649 additions and 928 deletions

View File

@ -20,8 +20,6 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:

View File

@ -1,25 +1,56 @@
# 🎀 Changelog # 🎀 Changelog
## [1.0.4] - 2021-03-12 ## [1.2.0] - 2022-03-17
### Added
- Job Management additions
- `job.start` and `job.done` hooks (`doc hooks job`)
- `hilbish.jobs` interface (`get(id)` function gets a job object via `id`, `all()` gets all)
- Customizable runner/exec mode
- However Hilbish runs interactive user input can now be changed Lua side (`doc runner-mode`)
### Changed
- `vimMode` doc is now `vim-mode`
### Fixed
- Make sure input which is supposed to go in history goes there
- Cursor is right at the end of input on history search
## [1.1.0] - 2022-03-17
### Added
- `hilbish.vimAction` hook (`doc vimMode actions`)
- `command.not-executable` hook (will replace `command.no-perm` in a future release)
### Fixed
- Check if interactive before adding to history
- Escape in vim mode exits all modes and not only insert
- Make 2nd line in prompt empty if entire prompt is 1 line
- Completion menu doesnt appear if there is only 1 result
- Ignore SIGQUIT, which caused a panic unhandled
- Remove hostname in greeting on Windows
- Handle PATH binaries properly on Windows
- Fix removal of dot in the beginning of folders/files that have them for file complete
- Fix prompt being set to the continue prompt even when exited
## [1.0.4] - 2022-03-12
### Fixed ### Fixed
- Panic when history directory doesn't exist - Panic when history directory doesn't exist
## [1.0.3] - 2021-03-12 ## [1.0.3] - 2022-03-12
### Fixed ### Fixed
- Removed duplicate executable suggestions - Removed duplicate executable suggestions
- User input is added to history now instead of what's ran by Hilbish - User input is added to history now instead of what's ran by Hilbish
- Formatting issue with prompt on no input - Formatting issue with prompt on no input
## [1.0.2] - 2021-03-06 ## [1.0.2] - 2022-03-06
### Fixed ### Fixed
- Cases where Hilbish's history directory doesn't exist will no longer cause a panic - Cases where Hilbish's history directory doesn't exist will no longer cause a panic
## [1.0.1] - 2021-03-06 ## [1.0.1] - 2022-03-06
### Fixed ### Fixed
- Using `hilbish.appendPath` will no longer result in string spam (debugging thing left being) - Using `hilbish.appendPath` will no longer result in string spam (debugging thing left being)
- Prompt gets set properly on startup - Prompt gets set properly on startup
## [1.0.0] - 2021-03-06 ## [1.0.0] - 2022-03-06
### Added ### Added
- MacOS is now officialy supported, default compile time vars have been added - MacOS is now officialy supported, default compile time vars have been added
for it for it
@ -392,6 +423,7 @@ This input for example will prompt for more input to complete:
First "stable" release of Hilbish. First "stable" release of Hilbish.
[1.1.0]: https://github.com/Rosettea/Hilbish/compare/v1.0.4...v1.1.0
[1.0.4]: https://github.com/Rosettea/Hilbish/compare/v1.0.3...v1.0.4 [1.0.4]: https://github.com/Rosettea/Hilbish/compare/v1.0.3...v1.0.4
[1.0.3]: https://github.com/Rosettea/Hilbish/compare/v1.0.2...v1.0.3 [1.0.3]: https://github.com/Rosettea/Hilbish/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/Rosettea/Hilbish/compare/v1.0.1...v1.0.2 [1.0.2]: https://github.com/Rosettea/Hilbish/compare/v1.0.1...v1.0.2

View File

@ -1,31 +1,30 @@
PREFIX ?= /usr PREFIX ?= /usr
DESTDIR ?=
BINDIR ?= $(PREFIX)/bin BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/share/hilbish LIBDIR ?= $(PREFIX)/share/hilbish
build: MY_GOFLAGS = -ldflags "-s -w"
@go build -ldflags "-s -w"
dev: all: dev
@go build -ldflags "-s -w -X main.version=$(shell git describe --tags)"
dev: MY_GOFLAGS = -ldflags "-s -w -X main.version=$(shell git describe --tags)"
dev: build
build:
go build $(MY_GOFLAGS)
install: install:
@install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v hilbish "$(DESTDIR)$(BINDIR)/hilbish" install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v hilbish "$(DESTDIR)$(BINDIR)/hilbish"
@mkdir -p "$(DESTDIR)$(LIBDIR)" mkdir -p "$(DESTDIR)$(LIBDIR)"
@cp libs docs emmyLuaDocs prelude .hilbishrc.lua "$(DESTDIR)$(LIBDIR)" -r cp -r libs docs emmyLuaDocs prelude .hilbishrc.lua "$(DESTDIR)$(LIBDIR)"
@grep "$(DESTDIR)$(BINDIR)/hilbish" -qxF /etc/shells || echo "$(DESTDIR)$(BINDIR)/hilbish" >> /etc/shells grep -qxF "$(DESTDIR)$(BINDIR)/hilbish" /etc/shells || echo "$(DESTDIR)$(BINDIR)/hilbish" >> /etc/shells
@echo "Hilbish Installed"
uninstall: uninstall:
@rm -vrf \ rm -vrf \
"$(DESTDIR)$(BINDIR)/hilbish" \ "$(DESTDIR)$(BINDIR)/hilbish" \
"$(DESTDIR)$(LIBDIR)" "$(DESTDIR)$(LIBDIR)"
@sed -i '/hilbish/d' /etc/shells sed -i '/hilbish/d' /etc/shells
@echo "Hilbish Uninstalled"
clean: clean:
@go clean go clean
all: build install .PHONY: all dev build install uninstall clean
.PHONY: install uninstall build dev clean

View File

@ -2,7 +2,7 @@
<img src="./assets/hilbish-flower.png" width=128><br> <img src="./assets/hilbish-flower.png" width=128><br>
<img src="./assets/hilbish-text.png" width=256><br> <img src="./assets/hilbish-text.png" width=256><br>
<blockquote> <blockquote>
🌺 The flower shell. A comfy and nice little shell for Lua users and fans! 🌺 The flower shell. A comfy and nice little shell for Lua fans!
</blockquote> </blockquote>
<p align="center"> <p align="center">
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/Rosettea/Hilbish?style=flat-square"> <img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/Rosettea/Hilbish?style=flat-square">
@ -14,12 +14,25 @@
</p> </p>
</div> </div>
Hilbish is a Unix-y shell which uses Lua for scripting. Things like the prompt, Hilbish is a extensible shell (framework). It was made to be very customizable
general configuration and such are done with Lua. via the Lua programming language. It aims to be easy to use for the casual
people but powerful for those who want to tinker more with their shell,
the thing used to interface with most of the system.
For interactive use, it uses a library to run sh which works on all The motivation for choosing Lua was that its simpler and better to use
platforms Hilbish can be compiled for. It can also act as a Lua REPL if you want than old shell script. It's fine for basic interactive shell uses,
it to be. but that's the only place Hilbish has shell script; everything else is Lua
and aims to be infinitely configurable. If something isn't, open an issue!
# Table of Contents
- [Screenshots](#Screenshots)
- [Installation](#Installation)
- [Prebuilt Bins](#Prebuilt-binaries)
- [AUR](#AUR)
- [Nixpkgs](#Nixpkgs)
- [Manual Build](#Manual-Build)
- [Getting Started](#Getting-Started)
- [Contributing](#Contributing)
# Screenshots # Screenshots
<div align="center"> <div align="center">
@ -29,8 +42,6 @@ it to be.
</div> </div>
# Installation # Installation
**NOTE:** Hilbish is currently only officially supported and tested on Linux
## Prebuilt binaries ## Prebuilt binaries
Go [here](https://nightly.link/Rosettea/Hilbish/workflows/build/master) for Go [here](https://nightly.link/Rosettea/Hilbish/workflows/build/master) for
builds on the master branch. builds on the master branch.
@ -56,7 +67,7 @@ If you're new to nix you should probably read up on how to do that [here](https:
### Prerequisites ### Prerequisites
- [Go 1.17+](https://go.dev) - [Go 1.17+](https://go.dev)
#### Build ### Build
First, clone Hilbish. The recursive is required, as some Lua libraries First, clone Hilbish. The recursive is required, as some Lua libraries
are submodules. are submodules.
```sh ```sh
@ -78,13 +89,27 @@ make build
After you did all that, run `sudo make install` to install Hilbish globally. After you did all that, run `sudo make install` to install Hilbish globally.
# Getting Started
At startup, you should see a message which says to run a `guide` command.
This guide is a *very* simple and basic step through text of what Hilbish is
and where to find documentation.
Documentation is primarily viewed via the in shell `doc` command.
Autogenerated function docs and general docs about other things are included
there, so be sure to read it.
Using Hilbish is the same as using any other Linux shell, with an addition
that you can also run Lua. Hilbish can also act as an enhanced Lua REPL
via `hilbish.runnerMode 'lua'`. To switch back to normal, use
`hilbish.runnerMode 'hybrid'`.
# Contributing # Contributing
Any kind of contributions to Hilbish are welcome! Any kind of contributions are welcome! Hilbish is very easy to contribute to.
Read [CONTRIBUTING.md](CONTRIBUTING.md) before getting started. Read [CONTRIBUTING.md](CONTRIBUTING.md) as a guideline to doing so.
**Thanks to everyone below who's contributed!** **Thanks to everyone below who's contributed!**
<a href="https://github.com/Hilbis/Hilbish/graphs/contributors"> <a href="https://github.com/Rosettea/Hilbish/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Hilbis/Hilbish" /> <img src="https://contrib.rocks/image?repo=Rosettea/Hilbish" />
</a> </a>
*Made with [contributors-img](https://contrib.rocks).* *Made with [contributors-img](https://contrib.rocks).*

View File

@ -4,57 +4,59 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/yuin/gopher-lua" "hilbish/util"
rt "github.com/arnodel/golua/runtime"
) )
var aliases *hilbishAliases var aliases *aliasHandler
type hilbishAliases struct { type aliasHandler struct {
aliases map[string]string aliases map[string]string
mu *sync.RWMutex mu *sync.RWMutex
} }
// initialize aliases map // initialize aliases map
func NewAliases() *hilbishAliases { func newAliases() *aliasHandler {
return &hilbishAliases{ return &aliasHandler{
aliases: make(map[string]string), aliases: make(map[string]string),
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
} }
} }
func (h *hilbishAliases) Add(alias, cmd string) { func (a *aliasHandler) Add(alias, cmd string) {
h.mu.Lock() a.mu.Lock()
defer h.mu.Unlock() defer a.mu.Unlock()
h.aliases[alias] = cmd a.aliases[alias] = cmd
} }
func (h *hilbishAliases) All() map[string]string { func (a *aliasHandler) All() map[string]string {
return h.aliases return a.aliases
} }
func (h *hilbishAliases) Delete(alias string) { func (a *aliasHandler) Delete(alias string) {
h.mu.Lock() a.mu.Lock()
defer h.mu.Unlock() defer a.mu.Unlock()
delete(h.aliases, alias) delete(a.aliases, alias)
} }
func (h *hilbishAliases) Resolve(cmdstr string) string { func (a *aliasHandler) Resolve(cmdstr string) string {
h.mu.RLock() a.mu.RLock()
defer h.mu.RUnlock() defer a.mu.RUnlock()
args := strings.Split(cmdstr, " ") args := strings.Split(cmdstr, " ")
for h.aliases[args[0]] != "" { for a.aliases[args[0]] != "" {
alias := h.aliases[args[0]] alias := a.aliases[args[0]]
cmdstr = alias + strings.TrimPrefix(cmdstr, args[0]) cmdstr = alias + strings.TrimPrefix(cmdstr, args[0])
cmdArgs, _ := splitInput(cmdstr) cmdArgs, _ := splitInput(cmdstr)
args = cmdArgs args = cmdArgs
if h.aliases[args[0]] == alias { if a.aliases[args[0]] == alias {
break break
} }
if h.aliases[args[0]] != "" { if a.aliases[args[0]] != "" {
continue continue
} }
} }
@ -64,41 +66,38 @@ func (h *hilbishAliases) Resolve(cmdstr string) string {
// lua section // lua section
func (h *hilbishAliases) Loader(L *lua.LState) *lua.LTable { func (a *aliasHandler) Loader(rtm *rt.Runtime) *rt.Table {
// create a lua module with our functions // create a lua module with our functions
hshaliasesLua := map[string]lua.LGFunction{ hshaliasesLua := map[string]util.LuaExport{
"add": h.luaAdd, "add": util.LuaExport{hlalias, 2, false},
"list": h.luaList, "list": util.LuaExport{a.luaList, 0, false},
"del": h.luaDelete, "del": util.LuaExport{a.luaDelete, 1, false},
} }
mod := L.SetFuncs(L.NewTable(), hshaliasesLua) mod := rt.NewTable()
util.SetExports(rtm, mod, hshaliasesLua)
return mod return mod
} }
func (h *hilbishAliases) luaAdd(L *lua.LState) int { func (a *aliasHandler) luaList(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
alias := L.CheckString(1) aliasesList := rt.NewTable()
cmd := L.CheckString(2) for k, v := range a.All() {
h.Add(alias, cmd) aliasesList.Set(rt.StringValue(k), rt.StringValue(v))
return 0
}
func (h *hilbishAliases) luaList(L *lua.LState) int {
aliasesList := L.NewTable()
for k, v := range h.All() {
aliasesList.RawSetString(k, lua.LString(v))
} }
L.Push(aliasesList) return c.PushingNext1(t.Runtime, rt.TableValue(aliasesList)), nil
return 1
} }
func (h *hilbishAliases) luaDelete(L *lua.LState) int { func (a *aliasHandler) luaDelete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
alias := L.CheckString(1) if err := c.Check1Arg(); err != nil {
h.Delete(alias) return nil, err
}
alias, err := c.StringArg(0)
if err != nil {
return nil, err
}
a.Delete(alias)
return 0 return c.Next(), nil
} }

716
api.go
View File

@ -4,6 +4,8 @@
package main package main
import ( import (
"bytes"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -14,182 +16,128 @@ import (
"hilbish/util" "hilbish/util"
"github.com/yuin/gopher-lua" rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/maxlandon/readline" "github.com/maxlandon/readline"
"github.com/blackfireio/osinfo" "github.com/blackfireio/osinfo"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
) )
var exports = map[string]lua.LGFunction { var exports = map[string]util.LuaExport{
"alias": hlalias, "alias": {hlalias, 2, false},
"appendPath": hlappendPath, "appendPath": {hlappendPath, 1, false},
"complete": hlcomplete, "complete": {hlcomplete, 2, false},
"cwd": hlcwd, "cwd": {hlcwd, 0, false},
"exec": hlexec, "exec": {hlexec, 1, false},
"goro": hlgoro, "runnerMode": {hlrunnerMode, 1, false},
"multiprompt": hlmlprompt, "goro": {hlgoro, 1, true},
"prependPath": hlprependPath, "highlighter": {hlhighlighter, 1, false},
"prompt": hlprompt, "hinter": {hlhinter, 1, false},
"inputMode": hlinputMode, "multiprompt": {hlmultiprompt, 1, false},
"interval": hlinterval, "prependPath": {hlprependPath, 1, false},
"read": hlread, "prompt": {hlprompt, 1, true},
"run": hlrun, "inputMode": {hlinputMode, 1, false},
"timeout": hltimeout, "interval": {hlinterval, 2, false},
"which": hlwhich, "read": {hlread, 1, false},
"run": {hlrun, 1, true},
"timeout": {hltimeout, 2, false},
"which": {hlwhich, 1, false},
} }
var greeting string var greeting string
var hshMod *lua.LTable var hshMod *rt.Table
var hilbishLoader = packagelib.Loader{
Load: hilbishLoad,
Name: "hilbish",
}
func hilbishLoader(L *lua.LState) int { func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
mod := L.SetFuncs(L.NewTable(), exports) mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
hshMod = mod hshMod = mod
host, _ := os.Hostname() host, _ := os.Hostname()
username := curuser.Username username := curuser.Username
greeting = `Welcome to {magenta}Hilbish{reset}, {cyan}` + curuser.Username + `{reset}.
The nice lil shell for {blue}Lua{reset} fanatics!
Check out the {blue}{bold}guide{reset} command to get started.
`
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows
} }
util.SetField(L, mod, "ver", lua.LString(version), "Hilbish version") greeting = `Welcome to {magenta}Hilbish{reset}, {cyan}` + username + `{reset}.
util.SetField(L, mod, "user", lua.LString(username), "Username of user") The nice lil shell for {blue}Lua{reset} fanatics!
util.SetField(L, mod, "host", lua.LString(host), "Host name of the machine") Check out the {blue}{bold}guide{reset} command to get started.
util.SetField(L, mod, "home", lua.LString(curuser.HomeDir), "Home directory of the user") `
util.SetField(L, mod, "dataDir", lua.LString(dataDir), "Directory for Hilbish's data files") util.SetField(rtm, mod, "ver", rt.StringValue(version), "Hilbish version")
util.SetField(L, mod, "interactive", lua.LBool(interactive), "If this is an interactive shell") util.SetField(rtm, mod, "user", rt.StringValue(username), "Username of user")
util.SetField(L, mod, "login", lua.LBool(interactive), "Whether this is a login shell") util.SetField(rtm, mod, "host", rt.StringValue(host), "Host name of the machine")
util.SetField(L, mod, "greeting", lua.LString(greeting), "Hilbish's welcome message for interactive shells. It has Lunacolors formatting.") util.SetField(rtm, mod, "home", rt.StringValue(curuser.HomeDir), "Home directory of the user")
util.SetField(l, mod, "vimMode", lua.LNil, "Current Vim mode of Hilbish (nil if not in Vim mode)") util.SetField(rtm, mod, "dataDir", rt.StringValue(dataDir), "Directory for Hilbish's data files")
util.SetField(l, hshMod, "exitCode", lua.LNumber(0), "Exit code of last exected command") util.SetField(rtm, mod, "interactive", rt.BoolValue(interactive), "If this is an interactive shell")
util.Document(L, mod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.") util.SetField(rtm, mod, "login", rt.BoolValue(login), "Whether this is a login shell")
util.SetField(rtm, mod, "greeting", rt.StringValue(greeting), "Hilbish's welcome message for interactive shells. It has Lunacolors formatting.")
util.SetField(rtm, mod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)")
util.SetField(rtm, hshMod, "exitCode", rt.IntValue(0), "Exit code of last exected command")
util.Document(mod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.")
// hilbish.userDir table // hilbish.userDir table
hshuser := L.NewTable() hshuser := rt.NewTable()
util.SetField(L, hshuser, "config", lua.LString(confDir), "User's config directory") util.SetField(rtm, hshuser, "config", rt.StringValue(confDir), "User's config directory")
util.SetField(L, hshuser, "data", lua.LString(userDataDir), "XDG data directory") util.SetField(rtm, hshuser, "data", rt.StringValue(userDataDir), "XDG data directory")
util.Document(L, hshuser, "User directories to store configs and/or modules.") util.Document(hshuser, "User directories to store configs and/or modules.")
L.SetField(mod, "userDir", hshuser) mod.Set(rt.StringValue("userDir"), rt.TableValue(hshuser))
// hilbish.os table // hilbish.os table
hshos := L.NewTable() hshos := rt.NewTable()
info, _ := osinfo.GetOSInfo() info, _ := osinfo.GetOSInfo()
util.SetField(L, hshos, "family", lua.LString(info.Family), "Family name of the current OS") util.SetField(rtm, hshos, "family", rt.StringValue(info.Family), "Family name of the current OS")
util.SetField(L, hshos, "name", lua.LString(info.Name), "Pretty name of the current OS") util.SetField(rtm, hshos, "name", rt.StringValue(info.Name), "Pretty name of the current OS")
util.SetField(L, hshos, "version", lua.LString(info.Version), "Version of the current OS") util.SetField(rtm, hshos, "version", rt.StringValue(info.Version), "Version of the current OS")
util.Document(L, hshos, "OS info interface") util.Document(hshos, "OS info interface")
L.SetField(mod, "os", hshos) mod.Set(rt.StringValue("os"), rt.TableValue(hshos))
// hilbish.aliases table // hilbish.aliases table
aliases = NewAliases() aliases = newAliases()
aliasesModule := aliases.Loader(L) aliasesModule := aliases.Loader(rtm)
util.Document(L, aliasesModule, "Alias inferface for Hilbish.") util.Document(aliasesModule, "Alias inferface for Hilbish.")
L.SetField(mod, "aliases", aliasesModule) mod.Set(rt.StringValue("aliases"), rt.TableValue(aliasesModule))
// hilbish.history table // hilbish.history table
historyModule := lr.Loader(L) historyModule := lr.Loader(rtm)
util.Document(L, historyModule, "History interface for Hilbish.") mod.Set(rt.StringValue("history"), rt.TableValue(historyModule))
L.SetField(mod, "history", historyModule) util.Document(historyModule, "History interface for Hilbish.")
// hilbish.completions table // hilbish.completion table
hshcomp := L.NewTable() hshcomp := rt.NewTable()
util.SetField(rtm, hshcomp, "files",
rt.FunctionValue(rt.NewGoFunction(luaFileComplete, "files", 3, false)),
"Completer for files")
util.SetField(L, hshcomp, "files", L.NewFunction(luaFileComplete), "Completer for files") util.SetField(rtm, hshcomp, "bins",
util.SetField(L, hshcomp, "bins", L.NewFunction(luaBinaryComplete), "Completer for executables/binaries") rt.FunctionValue(rt.NewGoFunction(luaBinaryComplete, "bins", 3, false)),
util.Document(L, hshcomp, "Completions interface for Hilbish.") "Completer for executables/binaries")
L.SetField(mod, "completion", hshcomp)
L.Push(mod) util.Document(hshcomp, "Completions interface for Hilbish.")
mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp))
return 1 // hilbish.runner table
} runnerModule := runnerModeLoader(rtm)
util.Document(runnerModule, "Runner/exec interface for Hilbish.")
mod.Set(rt.StringValue("runner"), rt.TableValue(runnerModule))
func luaFileComplete(L *lua.LState) int { // hilbish.jobs table
query := L.CheckString(1) jobs = newJobHandler()
ctx := L.CheckString(2) jobModule := jobs.loader(rtm)
fields := L.CheckTable(3) util.Document(jobModule, "(Background) job interface.")
mod.Set(rt.StringValue("jobs"), rt.TableValue(jobModule))
var fds []string timers = newTimerHandler()
fields.ForEach(func(k lua.LValue, v lua.LValue) { timerModule := timers.loader(rtm)
fds = append(fds, v.String()) util.Document(timerModule, "Timer interface, for control of all intervals and timeouts.")
}) mod.Set(rt.StringValue("timers"), rt.TableValue(timerModule))
completions := fileComplete(query, ctx, fds) return rt.TableValue(mod), nil
luaComps := L.NewTable()
for _, comp := range completions {
luaComps.Append(lua.LString(comp))
}
L.Push(luaComps)
return 1
}
func luaBinaryComplete(L *lua.LState) int {
query := L.CheckString(1)
ctx := L.CheckString(2)
fields := L.CheckTable(3)
var fds []string
fields.ForEach(func(k lua.LValue, v lua.LValue) {
fds = append(fds, v.String())
})
completions, _ := binaryComplete(query, ctx, fds)
luaComps := L.NewTable()
for _, comp := range completions {
luaComps.Append(lua.LString(comp))
}
L.Push(luaComps)
return 1
}
func setVimMode(mode string) {
util.SetField(l, hshMod, "vimMode", lua.LString(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)")
hooks.Em.Emit("hilbish.vimMode", mode)
}
func unsetVimMode() {
util.SetField(l, hshMod, "vimMode", lua.LNil, "Current Vim mode of Hilbish (nil if not in Vim mode)")
}
// run(cmd)
// Runs `cmd` in Hilbish's sh interpreter.
// --- @param cmd string
func hlrun(L *lua.LState) int {
var exitcode uint8
cmd := L.CheckString(1)
err := execCommand(cmd, cmd)
if code, ok := interp.IsExitStatus(err); ok {
exitcode = code
} else if err != nil {
exitcode = 1
}
L.Push(lua.LNumber(exitcode))
return 1
}
// cwd()
// Returns the current directory of the shell
func hlcwd(L *lua.LState) int {
cwd, _ := os.Getwd()
L.Push(lua.LString(cwd))
return 1
} }
func getenv(key, fallback string) string { func getenv(key, fallback string) string {
@ -200,28 +148,164 @@ func getenv(key, fallback string) string {
return value return value
} }
func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
query, ctx, fds, err := getCompleteParams(t, c)
if err != nil {
return nil, err
}
completions, _ := fileComplete(query, ctx, fds)
luaComps := rt.NewTable()
for i, comp := range completions {
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
}
return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), nil
}
func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
query, ctx, fds, err := getCompleteParams(t, c)
if err != nil {
return nil, err
}
completions, _ := binaryComplete(query, ctx, fds)
luaComps := rt.NewTable()
for i, comp := range completions {
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
}
return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), nil
}
func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, error) {
if err := c.CheckNArgs(3); err != nil {
return "", "", []string{}, err
}
query, err := c.StringArg(0)
if err != nil {
return "", "", []string{}, err
}
ctx, err := c.StringArg(1)
if err != nil {
return "", "", []string{}, err
}
fields, err := c.TableArg(2)
if err != nil {
return "", "", []string{}, err
}
var fds []string
nextVal := rt.NilValue
for {
next, val, ok := fields.Next(nextVal)
if next == rt.NilValue {
break
}
nextVal = next
valStr, ok := val.TryString()
if !ok {
continue
}
fds = append(fds, valStr)
}
return query, ctx, fds, err
}
func setVimMode(mode string) {
util.SetField(l, hshMod, "vimMode", rt.StringValue(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)")
hooks.Em.Emit("hilbish.vimMode", mode)
}
func unsetVimMode() {
util.SetField(l, hshMod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)")
}
// run(cmd, returnOut) -> exitCode, stdout, stderr
// Runs `cmd` in Hilbish's sh interpreter.
// If returnOut is true, the outputs of `cmd` will be returned as the 2nd and
// 3rd values instead of being outputted to the terminal.
// --- @param cmd string
func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
var terminalOut bool
if len(c.Etc()) != 0 {
tout := c.Etc()[0]
termOut, ok := tout.TryBool()
terminalOut = termOut
if !ok {
return nil, errors.New("bad argument to run (expected boolean, got " + tout.TypeName() + ")")
}
} else {
terminalOut = true
}
var exitcode uint8
stdout, stderr, err := execCommand(cmd, terminalOut)
if code, ok := interp.IsExitStatus(err); ok {
exitcode = code
} else if err != nil {
exitcode = 1
}
stdoutStr := ""
stderrStr := ""
if !terminalOut {
stdoutStr = stdout.(*bytes.Buffer).String()
stderrStr = stderr.(*bytes.Buffer).String()
}
return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil
}
// cwd()
// Returns the current directory of the shell
func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
cwd, _ := os.Getwd()
return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil
}
// read(prompt) -> input? // read(prompt) -> input?
// 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.
// Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen) // Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen)
// --- @param prompt string // --- @param prompt string
func hlread(L *lua.LState) int { func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
luaprompt := L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
luaprompt, err := c.StringArg(0)
if err != nil {
return nil, err
}
lualr := newLineReader("", true) lualr := newLineReader("", true)
lualr.SetPrompt(luaprompt) lualr.SetPrompt(luaprompt)
input, err := lualr.Read() input, err := lualr.Read()
if err != nil { if err != nil {
L.Push(lua.LNil) return c.Next(), nil
return 1
} }
L.Push(lua.LString(input)) return c.PushingNext1(t.Runtime, rt.StringValue(input)), nil
return 1
} }
/* /*
prompt(str) prompt(str, typ?)
Changes the shell prompt to `str` Changes the shell prompt to `str`
There are a few verbs that can be used in the prompt text. There are a few verbs that can be used in the prompt text.
These will be formatted and replaced with the appropriate values. These will be formatted and replaced with the appropriate values.
@ -229,53 +313,110 @@ These will be formatted and replaced with the appropriate values.
`%u` - Name of current user `%u` - Name of current user
`%h` - Hostname of device `%h` - Hostname of device
--- @param str string --- @param str string
--- @param typ string Type of prompt, being left or right. Left by default.
*/ */
func hlprompt(L *lua.LState) int { func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
prompt = L.CheckString(1) err := c.Check1Arg()
lr.SetPrompt(fmtPrompt(prompt)) 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() + ")")
}
}
return 0 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) // multiprompt(str)
// Changes the continued line prompt to `str` // Changes the continued line prompt to `str`
// --- @param str string // --- @param str string
func hlmlprompt(L *lua.LState) int { func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
multilinePrompt = L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
prompt, err := c.StringArg(0)
if err != nil {
return nil, err
}
multilinePrompt = prompt
return 0 return c.Next(), nil
} }
// alias(cmd, orig) // alias(cmd, orig)
// Sets an alias of `orig` to `cmd` // Sets an alias of `cmd` to `orig`
// --- @param cmd string // --- @param cmd string
// --- @param orig string // --- @param orig string
func hlalias(L *lua.LState) int { func hlalias(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
alias := L.CheckString(1) if err := c.CheckNArgs(2); err != nil {
source := L.CheckString(2) return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
orig, err := c.StringArg(1)
if err != nil {
return nil, err
}
aliases.Add(alias, source) aliases.Add(cmd, orig)
return 1 return c.Next(), nil
} }
// appendPath(dir) // appendPath(dir)
// Appends `dir` to $PATH // Appends `dir` to $PATH
// --- @param dir string|table // --- @param dir string|table
func hlappendPath(L *lua.LState) int { func hlappendPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
arg := c.Arg(0)
// check if dir is a table or a string // check if dir is a table or a string
arg := L.Get(1) if arg.Type() == rt.TableType {
if arg.Type() == lua.LTTable { nextVal := rt.NilValue
arg.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) { for {
appendPath(v.String()) next, val, ok := arg.AsTable().Next(nextVal)
}) if next == rt.NilValue {
} else if arg.Type() == lua.LTString { break
appendPath(arg.String()) }
nextVal = next
valStr, ok := val.TryString()
if !ok {
continue
}
appendPath(valStr)
}
} else if arg.Type() == rt.StringType {
appendPath(arg.AsString())
} else { } else {
L.RaiseError("bad argument to appendPath (expected string or table, got %v)", L.Get(1).Type().String()) return nil, errors.New("bad argument to appendPath (expected string or table, got " + arg.TypeName() + ")")
} }
return 0 return c.Next(), nil
} }
func appendPath(dir string) { func appendPath(dir string) {
@ -291,8 +432,14 @@ func appendPath(dir string) {
// exec(cmd) // exec(cmd)
// Replaces running hilbish with `cmd` // Replaces running hilbish with `cmd`
// --- @param cmd string // --- @param cmd string
func hlexec(L *lua.LState) int { func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
cmd := L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
cmdArgs, _ := splitInput(cmd) cmdArgs, _ := splitInput(cmd)
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
cmdPath, err := exec.LookPath(cmdArgs[0]) cmdPath, err := exec.LookPath(cmdArgs[0])
@ -314,88 +461,82 @@ func hlexec(L *lua.LState) int {
os.Exit(0) os.Exit(0)
} }
return 0 return c.Next(), nil
} }
// goro(fn) // goro(fn)
// Puts `fn` in a goroutine // Puts `fn` in a goroutine
// --- @param fn function // --- @param fn function
func hlgoro(L *lua.LState) int { func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
fn := L.CheckFunction(1) if err := c.Check1Arg(); err != nil {
argnum := L.GetTop() return nil, err
args := make([]lua.LValue, argnum) }
for i := 1; i <= argnum; i++ { fn, err := c.ClosureArg(0)
args[i - 1] = L.Get(i) if err != nil {
return nil, err
} }
// call fn // call fn
go func() { go func() {
if err := L.CallByParam(lua.P{ _, err := rt.Call1(l.MainThread(), rt.FunctionValue(fn), c.Etc()...)
Fn: fn, if err != nil {
NRet: 0,
Protect: true,
}, args...); err != nil {
fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err) fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err)
} }
}() }()
return 0 return c.Next(), nil
} }
// timeout(cb, time) // timeout(cb, time)
// Runs the `cb` function after `time` in milliseconds // Runs the `cb` function after `time` in milliseconds
// Returns a `timer` object (see `doc timers`).
// --- @param cb function // --- @param cb function
// --- @param time number // --- @param time number
func hltimeout(L *lua.LState) int { // --- @return table
cb := L.CheckFunction(1) func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
ms := L.CheckInt(2) if err := c.CheckNArgs(2); err != nil {
return nil, err
timeout := time.Duration(ms) * time.Millisecond
time.Sleep(timeout)
if err := L.CallByParam(lua.P{
Fn: cb,
NRet: 0,
Protect: true,
}); err != nil {
fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err)
} }
return 0 cb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
ms, err := c.IntArg(1)
if err != nil {
return nil, err
}
interval := time.Duration(ms) * time.Millisecond
timer := timers.create(timerTimeout, interval, cb)
timer.start()
return c.PushingNext1(t.Runtime, timer.lua()), nil
} }
// interval(cb, time) // interval(cb, time)
// Runs the `cb` function every `time` milliseconds // Runs the `cb` function every `time` milliseconds.
// Returns a `timer` object (see `doc timers`).
// --- @param cb function // --- @param cb function
// --- @param time number // --- @param time number
func hlinterval(L *lua.LState) int { // --- @return table
intervalfunc := L.CheckFunction(1) func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
ms := L.CheckInt(2) if err := c.CheckNArgs(2); err != nil {
return nil, err
}
cb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
ms, err := c.IntArg(1)
if err != nil {
return nil, err
}
interval := time.Duration(ms) * time.Millisecond interval := time.Duration(ms) * time.Millisecond
timer := timers.create(timerInterval, interval, cb)
timer.start()
ticker := time.NewTicker(interval) return c.PushingNext1(t.Runtime, timer.lua()), nil
stop := make(chan lua.LValue)
go func() {
for {
select {
case <-ticker.C:
if err := L.CallByParam(lua.P{
Fn: intervalfunc,
NRet: 0,
Protect: true,
}); err != nil {
fmt.Fprintln(os.Stderr, "Error in interval function:\n\n", err)
stop <- lua.LTrue // stop the interval
}
case <-stop:
ticker.Stop()
return
}
}
}()
L.Push(lua.LChannel(stop))
return 1
} }
// complete(scope, cb) // complete(scope, cb)
@ -408,20 +549,27 @@ func hlinterval(L *lua.LState) int {
// `grid` (the normal file completion display) or `list` (with a description) // `grid` (the normal file completion display) or `list` (with a description)
// --- @param scope string // --- @param scope string
// --- @param cb function // --- @param cb function
func hlcomplete(L *lua.LState) int { func hlcomplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
scope := L.CheckString(1) scope, cb, err := util.HandleStrCallback(t, c)
cb := L.CheckFunction(2) if err != nil {
return nil, err
}
luaCompletions[scope] = cb luaCompletions[scope] = cb
return 0 return c.Next(), nil
} }
// prependPath(dir) // prependPath(dir)
// Prepends `dir` to $PATH // Prepends `dir` to $PATH
// --- @param dir string // --- @param dir string
func hlprependPath(L *lua.LState) int { func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
dir := L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
dir, err := c.StringArg(0)
if err != nil {
return nil, err
}
dir = strings.Replace(dir, "~", curuser.HomeDir, 1) dir = strings.Replace(dir, "~", curuser.HomeDir, 1)
pathenv := os.Getenv("PATH") pathenv := os.Getenv("PATH")
@ -430,29 +578,40 @@ func hlprependPath(L *lua.LState) int {
os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv) os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv)
} }
return 0 return c.Next(), nil
} }
// which(binName) // which(binName)
// Searches for an executable called `binName` in the directories of $PATH // Searches for an executable called `binName` in the directories of $PATH
// --- @param binName string // --- @param binName string
func hlwhich(L *lua.LState) int { func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
binName := L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
binName, err := c.StringArg(0)
if err != nil {
return nil, err
}
path, err := exec.LookPath(binName) path, err := exec.LookPath(binName)
if err != nil { if err != nil {
l.Push(lua.LNil) return c.Next(), nil
return 1
} }
l.Push(lua.LString(path)) return c.PushingNext1(t.Runtime, rt.StringValue(path)), nil
return 1
} }
// inputMode(mode) // inputMode(mode)
// Sets the input mode for Hilbish's line reader. Accepts either emacs for vim // Sets the input mode for Hilbish's line reader. Accepts either emacs for vim
// --- @param mode string // --- @param mode string
func hlinputMode(L *lua.LState) int { func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
mode := L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
mode, err := c.StringArg(0)
if err != nil {
return nil, err
}
switch mode { switch mode {
case "emacs": case "emacs":
unsetVimMode() unsetVimMode()
@ -460,7 +619,74 @@ func hlinputMode(L *lua.LState) int {
case "vim": case "vim":
setVimMode("insert") setVimMode("insert")
lr.rl.InputMode = readline.Vim lr.rl.InputMode = readline.Vim
default: L.RaiseError("inputMode: expected vim or emacs, received " + mode) default:
return nil, errors.New("inputMode: expected vim or emacs, received " + mode)
} }
return 0
return c.Next(), nil
}
// runnerMode(mode)
// 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.
// Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
// sh, and lua. It also accepts a function, to which if it is passed one
// will call it to execute user input instead.
// --- @param mode string|function
func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
mode := c.Arg(0)
switch mode.Type() {
case rt.StringType:
switch mode.AsString() {
// no fallthrough doesnt work so eh
case "hybrid", "hybridRev", "lua", "sh": runnerMode = mode
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.AsString())
}
case rt.FunctionType: runnerMode = mode
default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.TypeName())
}
return c.Next(), nil
}
// hinter(cb)
// Sets the hinter function. This will be called on every key insert to determine
// what text to use as an inline hint. The callback is passed 2 arguments:
// the current line and the position. It is expected to return a string
// which will be used for the hint.
// --- @param cb function
func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
hinterCb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
hinter = hinterCb
return c.Next(), err
}
// highlighter(cb)
// Sets the highlighter function. This is mainly for syntax hightlighting, but in
// reality could set the input of the prompt to display anything. The callback
// is passed the current line as typed and is expected to return a line that will
// be used to display in the line.
// --- @param cb function
func hlhighlighter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
highlighterCb, err := c.ClosureArg(0)
if err != nil {
return nil, err
}
highlighter = highlighterCb
return c.Next(), err
} }

View File

@ -80,6 +80,9 @@ func main() {
if emmyType == "@param" { if emmyType == "@param" {
em.Params = append(em.Params, emmyLinePieces[1]) em.Params = append(em.Params, emmyLinePieces[1])
} }
if emmyType == "@vararg" {
em.Params = append(em.Params, "...") // add vararg
}
em.Docs = append(em.Docs, d) em.Docs = append(em.Docs, d)
} else { } else {
funcdoc = append(funcdoc, d) funcdoc = append(funcdoc, d)
@ -111,6 +114,9 @@ func main() {
if emmyType == "@param" { if emmyType == "@param" {
em.Params = append(em.Params, emmyLinePieces[1]) em.Params = append(em.Params, emmyLinePieces[1])
} }
if emmyType == "@vararg" {
em.Params = append(em.Params, "...") // add vararg
}
em.Docs = append(em.Docs, d) em.Docs = append(em.Docs, d)
} else { } else {
funcdoc = append(funcdoc, d) funcdoc = append(funcdoc, d)

View File

@ -2,27 +2,12 @@ package main
import ( import (
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"os" "os"
"unicode"
) )
func fileComplete(query, ctx string, fields []string) []string { func fileComplete(query, ctx string, fields []string) ([]string, string) {
var completions []string return matchPath(query)
prefixes := []string{"./", "../", "/", "~/"}
for _, prefix := range prefixes {
if strings.HasPrefix(query, prefix) {
completions, _ = matchPath(strings.Replace(query, "~", curuser.HomeDir, 1), query)
}
}
if len(completions) == 0 && len(fields) > 1 {
completions, _ = matchPath("./" + query, query)
}
return completions
} }
func binaryComplete(query, ctx string, fields []string) ([]string, string) { func binaryComplete(query, ctx string, fields []string) ([]string, string) {
@ -31,17 +16,17 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
prefixes := []string{"./", "../", "/", "~/"} prefixes := []string{"./", "../", "/", "~/"}
for _, prefix := range prefixes { for _, prefix := range prefixes {
if strings.HasPrefix(query, prefix) { if strings.HasPrefix(query, prefix) {
fileCompletions := fileComplete(query, ctx, fields) fileCompletions, filePref := matchPath(query)
if len(fileCompletions) != 0 { if len(fileCompletions) != 0 {
for _, f := range fileCompletions { for _, f := range fileCompletions {
name := strings.Replace(query + f, "~", curuser.HomeDir, 1) fullPath, _ := filepath.Abs(expandHome(query + strings.TrimPrefix(f, filePref)))
if info, err := os.Stat(name); err == nil && info.Mode().Perm() & 0100 == 0 { if err := findExecutable(fullPath, false, true); err != nil {
continue continue
} }
completions = append(completions, f) completions = append(completions, f)
} }
} }
return completions, "" return completions, filePref
} }
} }
@ -53,7 +38,8 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
// get basename from matches // get basename from matches
for _, match := range matches { for _, match := range matches {
// check if we have execute permissions for our match // check if we have execute permissions for our match
if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 { err := findExecutable(match, true, false)
if err != nil {
continue continue
} }
// get basename from match // get basename from match
@ -76,55 +62,53 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
return completions, query return completions, query
} }
func matchPath(path, pref string) ([]string, error) { func matchPath(query string) ([]string, string) {
var entries []string var entries []string
matches, err := filepath.Glob(desensitize(path) + "*") var baseName string
if err == nil {
args := []string{
"\"", "\\\"",
"'", "\\'",
"`", "\\`",
" ", "\\ ",
"(", "\\(",
")", "\\)",
"[", "\\[",
"]", "\\]",
}
r := strings.NewReplacer(args...) path, _ := filepath.Abs(expandHome(filepath.Dir(query)))
for _, match := range matches { if string(query) == "" {
name := filepath.Base(match) // filepath base below would give us "."
p := filepath.Base(pref) // which would cause a match of only dotfiles
if pref == "" { path, _ = filepath.Abs(".")
p = "" } else if !strings.HasSuffix(query, string(os.PathSeparator)) {
baseName = filepath.Base(query)
}
files, _ := os.ReadDir(path)
for _, file := range files {
if strings.HasPrefix(strings.ToLower(file.Name()), strings.ToLower(baseName)) {
entry := file.Name()
if file.IsDir() {
entry = entry + string(os.PathSeparator)
} }
name = strings.TrimPrefix(name, p) entry = escapeFilename(entry)
matchFull, _ := filepath.Abs(match) entries = append(entries, entry)
if info, err := os.Stat(matchFull); err == nil && info.IsDir() {
name = name + string(os.PathSeparator)
}
name = r.Replace(name)
entries = append(entries, name)
} }
} }
return entries, err return entries, baseName
} }
func desensitize(text string) string { func escapeFilename(fname string) string {
if runtime.GOOS == "windows" { args := []string{
return text "\"", "\\\"",
"'", "\\'",
"`", "\\`",
" ", "\\ ",
"(", "\\(",
")", "\\)",
"[", "\\[",
"]", "\\]",
"$", "\\$",
"&", "\\&",
"*", "\\*",
">", "\\>",
"<", "\\<",
"|", "\\|",
} }
p := strings.Builder{} r := strings.NewReplacer(args...)
return r.Replace(fname)
for _, r := range text {
if unicode.IsLetter(r) {
p.WriteString("[" + string(unicode.ToLower(r)) + string(unicode.ToUpper(r)) + "]")
} else {
p.WriteString(string(r))
}
}
return p.String()
} }

View File

@ -1,4 +1,4 @@
alias(cmd, orig) > Sets an alias of `orig` to `cmd` alias(cmd, orig) > Sets an alias of `cmd` to `orig`
appendPath(dir) > Appends `dir` to $PATH appendPath(dir) > Appends `dir` to $PATH
@ -16,15 +16,26 @@ exec(cmd) > Replaces running hilbish with `cmd`
goro(fn) > Puts `fn` in a goroutine goro(fn) > Puts `fn` in a goroutine
highlighter(cb) > Sets the highlighter function. This is mainly for syntax hightlighting, but in
reality could set the input of the prompt to display anything. The callback
is passed the current line as typed and is expected to return a line that will
be used to display in the line.
hinter(cb) > Sets the hinter function. This will be called on every key insert to determine
what text to use as an inline hint. The callback is passed 2 arguments:
the current line and the position. It is expected to return a string
which will be used for the hint.
inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs for vim inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs for vim
interval(cb, time) > Runs the `cb` function every `time` milliseconds interval(cb, time) > Runs the `cb` function every `time` milliseconds.
Returns a `timer` object (see `doc timers`).
multiprompt(str) > Changes the continued line prompt to `str` multiprompt(str) > Changes the continued line prompt to `str`
prependPath(dir) > Prepends `dir` to $PATH prependPath(dir) > Prepends `dir` to $PATH
prompt(str) > Changes the shell prompt to `str` prompt(str, typ?) > Changes the shell prompt to `str`
There are a few verbs that can be used in the prompt text. There are a few verbs that can be used in the prompt text.
These will be formatted and replaced with the appropriate values. These will be formatted and replaced with the appropriate values.
`%d` - Current working directory `%d` - Current working directory
@ -35,9 +46,18 @@ read(prompt) -> input? > Read input from the user, using Hilbish's line editor/i
This is a separate instance from the one Hilbish actually uses. 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 (which shouldn't happen) Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen)
run(cmd) > Runs `cmd` in Hilbish's sh interpreter. run(cmd, returnOut) -> exitCode, stdout, stderr > Runs `cmd` in Hilbish's sh interpreter.
If returnOut is true, the outputs of `cmd` will be returned as the 2nd and
3rd values instead of being outputted to the terminal.
runnerMode(mode) > 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.
Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
sh, and lua. It also accepts a function, to which if it is passed one
will call it to execute user input instead.
timeout(cb, time) > Runs the `cb` function after `time` in milliseconds timeout(cb, time) > Runs the `cb` function after `time` in milliseconds
Returns a `timer` object (see `doc timers`).
which(binName) > Searches for an executable called `binName` in the directories of $PATH which(binName) > Searches for an executable called `binName` in the directories of $PATH

13
docs/hooks/job.txt 100644
View File

@ -0,0 +1,13 @@
Note: A `job` is a table with the following keys:
- cmd: command string
- running: boolean whether the job is running
- id: unique id for the job
- pid: process id for the job
- exitCode: exit code of the job
In ordinary cases you'd prefer to use the id instead of pid. The id is unique to
Hilbish and is how you get jobs with the `hilbish.jobs` interface.
+ `job.start` -> job > Thrown when a new background job starts.
+ `job.done` -> job > Thrown when a background jobs exits.

View File

@ -0,0 +1,42 @@
Hilbish is *unique,* when interactive it first attempts to run input as
Lua and then tries shell script. But if you're normal, you wouldn't
really be using Hilbish anyway but you'd also not want this
(or maybe want Lua only in some cases.)
The "runner mode" of Hilbish is customizable via `hilbish.runnerMode`,
which determines how Hilbish will run user input. By default, this is
set to `hybrid` which is the previously mentioned behaviour of running Lua
first then going to shell script. If you want the reverse order, you can
set it to `hybridRev` and for isolated modes there is `sh` and `lua`
respectively.
You can also set it to a function, which will be called everytime Hilbish
needs to run interactive input. For example, you can set this to a simple
function to compile and evaluate Fennel, and now you can run Fennel.
You can even mix it with sh to make a hybrid mode with Lua replaced by
Fennel.
An example:
hilbish.runnerMode(function(input)
local ok = pcall(fennel.eval, input)
if ok then
return input, 0, nil
end
return hilbish.runner.sh(input)
end)
The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode`
and also provides the sh and Lua runner functions that Hilbish itself uses.
A runner function is expected to return 3 values: the input, exit code, and an error.
The input return is there incase you need to prompt for more input.
If you don't, just return the input passed to the runner function.
The exit code has to be a number, it will be 0 otherwise and the error can be
`nil` to indicate no error.
## Functions
These are the functions for the `hilbish.runner` interface
+ setMode(mode) > The same as `hilbish.runnerMode`
+ sh(input) -> input, code, err > Runs `input` in Hilbish's sh interpreter
+ lua(input) -> input, code, err > Evals `input` as Lua code

View File

@ -1,9 +1,9 @@
setRaw() > Puts the terminal in raw mode
restoreState() > Restores the last saved state of the terminal restoreState() > Restores the last saved state of the terminal
saveState() > Saves the current state of the terminal saveState() > Saves the current state of the terminal
setRaw() > Puts the terminal in raw mode
size() > Gets the dimensions of the terminal. Returns a table with `width` and `height` size() > Gets the dimensions of the terminal. Returns a table with `width` and `height`
Note: this is not the size in relation to the dimensions of the display Note: this is not the size in relation to the dimensions of the display

30
docs/timers.txt 100644
View File

@ -0,0 +1,30 @@
If you ever want to run a piece of code on a timed interval, or want to wait
a few seconds, you don't have to rely on timing tricks, as Hilbish has a
timer API to set intervals and timeouts.
These are the simple functions `hilbish.interval` and `hilbish.timeout` (doc
accessible with `doc hilbish`). But if you want slightly more control over
them, there is the `hilbish.timers` interface. It allows you to get
a timer via ID.
# Timer Interface
## Functions
- `get(id)` -> timer: get a timer via its id
- `create(type, ms, callback)` -> timer: creates a timer, adding it to the timer pool.
`type` is the type of timer it will be. 0 is an interval, 1 is a timeout.
`ms` is the time it will run for in seconds. callback is the function called
when the timer is triggered.
# Timer Object
Those previously mentioned functions return a `timer` object, to which you can
stop and start a timer again. The functions of the timers interface also
return a timer object.
## Properties
- `duration`: amount of time the timer runs for in milliseconds
- `running`: whether the timer is running or not
- `type`: the type of timer (0 is interval, 1 is timeout)
## Functions
- `stop()`: stops the timer. returns an error if it's already stopped
- `start()`: starts the timer. returns an error if it's already started

View File

@ -0,0 +1,16 @@
Vim actions are essentially just when a user uses a Vim keybind.
Things like yanking and pasting are Vim actions.
This is not an "offical Vim thing," just a Hilbish thing.
The `hilbish.vimAction` hook is thrown whenever a Vim action occurs.
It passes 2 arguments: the action name, and an array (table) of args
relating to it.
Here is documentation for what the table of args will hold for an
appropriate Vim action.
- `yank`: register, yankedText
The first argument for the yank action is the register yankedText goes to.
- `paste`: register, pastedText
The first argument for the paste action is the register pastedText is taken from.

View File

@ -0,0 +1,4 @@
Hilbish has a Vim binding input mode accessible for use.
It can be enabled with the `hilbish.inputMode` function (check `doc hilbish`).
This is documentation for everything relating to it.

View File

@ -15,6 +15,6 @@ function bait.catchOnce(name, cb) end
--- Throws a hook with `name` with the provided `args` --- Throws a hook with `name` with the provided `args`
--- @param name string --- @param name string
--- @vararg any --- @vararg any
function bait.throw(name) end function bait.throw(name, ...) end
return bait return bait

View File

@ -2,7 +2,7 @@
local hilbish = {} local hilbish = {}
--- Sets an alias of `orig` to `cmd` --- Sets an alias of `cmd` to `orig`
--- @param cmd string --- @param cmd string
--- @param orig string --- @param orig string
function hilbish.alias(cmd, orig) end function hilbish.alias(cmd, orig) end
@ -33,18 +33,34 @@ function hilbish.exec(cmd) end
--- @param fn function --- @param fn function
function hilbish.goro(fn) end function hilbish.goro(fn) end
--- Sets the highlighter function. This is mainly for syntax hightlighting, but in
--- reality could set the input of the prompt to display anything. The callback
--- is passed the current line as typed and is expected to return a line that will
--- be used to display in the line.
--- @param cb function
function hilbish.highlighter(cb) end
--- Sets the hinter function. This will be called on every key insert to determine
--- what text to use as an inline hint. The callback is passed 2 arguments:
--- the current line and the position. It is expected to return a string
--- which will be used for the hint.
--- @param cb function
function hilbish.hinter(cb) end
--- Sets the input mode for Hilbish's line reader. Accepts either emacs for vim --- Sets the input mode for Hilbish's line reader. Accepts either emacs for vim
--- @param mode string --- @param mode string
function hilbish.inputMode(mode) end function hilbish.inputMode(mode) end
--- Runs the `cb` function every `time` milliseconds --- Runs the `cb` function every `time` milliseconds.
--- Returns a `timer` object (see `doc timers`).
--- @param cb function --- @param cb function
--- @param time number --- @param time number
--- @return table
function hilbish.interval(cb, time) end function hilbish.interval(cb, time) end
--- Changes the continued line prompt to `str` --- Changes the continued line prompt to `str`
--- @param str string --- @param str string
function hilbish.mlprompt(str) end function hilbish.multiprompt(str) end
--- Prepends `dir` to $PATH --- Prepends `dir` to $PATH
--- @param dir string --- @param dir string
@ -57,7 +73,8 @@ function hilbish.prependPath(dir) end
--- `%u` - Name of current user --- `%u` - Name of current user
--- `%h` - Hostname of device --- `%h` - Hostname of device
--- @param str string --- @param str string
function hilbish.prompt(str) end --- @param typ string Type of prompt, being left or right. Left by default.
function hilbish.prompt(str, typ) end
--- 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.
@ -66,12 +83,24 @@ function hilbish.prompt(str) end
function hilbish.read(prompt) end function hilbish.read(prompt) end
--- Runs `cmd` in Hilbish's sh interpreter. --- Runs `cmd` in Hilbish's sh interpreter.
--- If returnOut is true, the outputs of `cmd` will be returned as the 2nd and
--- 3rd values instead of being outputted to the terminal.
--- @param cmd string --- @param cmd string
function hilbish.run(cmd) end function hilbish.run(cmd) end
--- 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.
--- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
--- sh, and lua. It also accepts a function, to which if it is passed one
--- will call it to execute user input instead.
--- @param mode string|function
function hilbish.runnerMode(mode) end
--- Runs the `cb` function after `time` in milliseconds --- Runs the `cb` function after `time` in milliseconds
--- Returns a `timer` object (see `doc timers`).
--- @param cb function --- @param cb function
--- @param time number --- @param time number
--- @return table
function hilbish.timeout(cb, time) end function hilbish.timeout(cb, time) end
--- Searches for an executable called `binName` in the directories of $PATH --- Searches for an executable called `binName` in the directories of $PATH

View File

@ -2,15 +2,15 @@
local terminal = {} local terminal = {}
--- Puts the terminal in raw mode
function terminal.raw() end
--- Restores the last saved state of the terminal --- Restores the last saved state of the terminal
function terminal.restoreState() end function terminal.restoreState() end
--- Saves the current state of the terminal --- Saves the current state of the terminal
function terminal.saveState() end function terminal.saveState() end
--- Puts the terminal in raw mode
function terminal.setRaw() end
--- Gets the dimensions of the terminal. Returns a table with `width` and `height` --- Gets the dimensions of the terminal. Returns a table with `width` and `height`
--- Note: this is not the size in relation to the dimensions of the display --- Note: this is not the size in relation to the dimensions of the display
function terminal.size() end function terminal.size() end

363
exec.go
View File

@ -1,30 +1,109 @@
package main package main
import ( import (
"bytes"
"context" "context"
"errors"
"os/exec"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"syscall"
"time" "time"
"hilbish/util" "hilbish/util"
"github.com/yuin/gopher-lua" rt "github.com/arnodel/golua/runtime"
"mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/shell"
//"github.com/yuin/gopher-lua/parse" //"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
) )
func runInput(input, origInput string) { var errNotExec = errors.New("not executable")
var runnerMode rt.Value = rt.StringValue("hybrid")
func runInput(input string, priv bool) {
running = true running = true
cmdString := aliases.Resolve(input) cmdString := aliases.Resolve(input)
hooks.Em.Emit("command.preexec", input, cmdString) hooks.Em.Emit("command.preexec", input, cmdString)
var exitCode uint8
var err error
if runnerMode.Type() == rt.StringType {
switch runnerMode.AsString() {
case "hybrid":
_, _, err = handleLua(cmdString)
if err == nil {
cmdFinish(0, input, priv)
return
}
input, exitCode, err = handleSh(input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
case "hybridRev":
_, _, err = handleSh(input)
if err == nil {
cmdFinish(0, input, priv)
return
}
input, exitCode, err = handleLua(cmdString)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
case "lua":
input, exitCode, err = handleLua(cmdString)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
case "sh":
input, exitCode, err = handleSh(input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
cmdFinish(exitCode, input, priv)
}
} else {
// can only be a string or function so
term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 2, false)
err := rt.Call(l.MainThread(), runnerMode, []rt.Value{rt.StringValue(cmdString)}, term)
if err != nil {
fmt.Fprintln(os.Stderr, err)
cmdFinish(124, input, priv)
return
}
luaexitcode := term.Get(0)
runErr := term.Get(1)
luaInput := term.Get(1)
var exitCode uint8
if code, ok := luaexitcode.TryInt(); ok {
exitCode = uint8(code)
}
if inp, ok := luaInput.TryString(); ok {
input = inp
}
if runErr != rt.NilValue {
fmt.Fprintln(os.Stderr, runErr)
}
cmdFinish(exitCode, input, priv)
}
}
func handleLua(cmdString string) (string, uint8, error) {
// First try to load input, essentially compiling to bytecode // First try to load input, essentially compiling to bytecode
fn, err := l.LoadString(cmdString) chunk, err := l.CompileAndLoadLuaChunk("", []byte(cmdString), rt.TableValue(l.GlobalEnv()))
if err != nil && noexecute { if err != nil && noexecute {
fmt.Println(err) fmt.Println(err)
/* if lerr, ok := err.(*lua.ApiError); ok { /* if lerr, ok := err.(*lua.ApiError); ok {
@ -33,62 +112,102 @@ func runInput(input, origInput string) {
} }
} }
*/ */
return return cmdString, 125, err
} }
// And if there's no syntax errors and -n isnt provided, run // And if there's no syntax errors and -n isnt provided, run
if !noexecute { if !noexecute {
l.Push(fn) if chunk != nil {
err = l.PCall(0, lua.MultRet, nil) _, err = rt.Call1(l.MainThread(), rt.FunctionValue(chunk))
}
} }
if err == nil { if err == nil {
cmdFinish(0, cmdString, origInput) return cmdString, 0, nil
return
} }
// Last option: use sh interpreter return cmdString, 125, err
err = execCommand(cmdString, origInput) }
func handleSh(cmdString string) (string, uint8, error) {
_, _, err := execCommand(cmdString, true)
if err != nil { if err != nil {
// If input is incomplete, start multiline prompting // If input is incomplete, start multiline prompting
if syntax.IsIncomplete(err) { if syntax.IsIncomplete(err) {
if !interactive {
return cmdString, 126, err
}
for { for {
cmdString, err = continuePrompt(strings.TrimSuffix(cmdString, "\\")) cmdString, err = continuePrompt(strings.TrimSuffix(cmdString, "\\"))
if err != nil { if err != nil {
break break
} }
err = execCommand(cmdString, origInput) _, _, err = execCommand(cmdString, true)
if syntax.IsIncomplete(err) || strings.HasSuffix(input, "\\") { if syntax.IsIncomplete(err) || strings.HasSuffix(cmdString, "\\") {
continue continue
} else if code, ok := interp.IsExitStatus(err); ok { } else if code, ok := interp.IsExitStatus(err); ok {
cmdFinish(code, cmdString, origInput) return cmdString, code, nil
} else if err != nil { } else if err != nil {
fmt.Fprintln(os.Stderr, err) return cmdString, 126, err
cmdFinish(1, cmdString, origInput)
} else { } else {
cmdFinish(0, cmdString, origInput) return cmdString, 0, nil
} }
break
} }
} else { } else {
if code, ok := interp.IsExitStatus(err); ok { if code, ok := interp.IsExitStatus(err); ok {
cmdFinish(code, cmdString, origInput) return cmdString, code, nil
} else { } else {
cmdFinish(126, cmdString, origInput) return cmdString, 126, err
fmt.Fprintln(os.Stderr, err)
} }
} }
} else {
cmdFinish(0, cmdString, origInput)
} }
return cmdString, 0, nil
} }
// Run command in sh interpreter // Run command in sh interpreter
func execCommand(cmd, old string) error { func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil { if err != nil {
return err return nil, nil, err
} }
exechandle := func(ctx context.Context, args []string) error { runner, _ := interp.New()
var stdout io.Writer
var stderr io.Writer
if terminalOut {
interp.StdIO(os.Stdin, os.Stdout, os.Stderr)(runner)
} else {
stdout = new(bytes.Buffer)
stderr = new(bytes.Buffer)
interp.StdIO(os.Stdin, stdout, stderr)(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)
}
interp.ExecHandler(execHandle(bg))(runner)
err = runner.Run(context.TODO(), stmt)
if err != nil {
return stdout, stderr, err
}
}
return stdout, stderr, nil
}
func execHandle(bg bool) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " ")) _, argstring := splitInput(strings.Join(args, " "))
// i dont really like this but it works // i dont really like this but it works
if aliases.All()[args[0]] != "" { if aliases.All()[args[0]] != "" {
@ -101,74 +220,176 @@ func execCommand(cmd, old string) error {
// If alias was found, use command alias // If alias was found, use command alias
argstring = aliases.Resolve(argstring) argstring = aliases.Resolve(argstring)
args, _ = shell.Fields(argstring, nil) var err error
args, err = shell.Fields(argstring, nil)
if err != nil {
return err
}
} }
// If command is defined in Lua then run it // If command is defined in Lua then run it
luacmdArgs := l.NewTable() luacmdArgs := rt.NewTable()
for _, str := range args[1:] { for i, str := range args[1:] {
luacmdArgs.Append(lua.LString(str)) luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
} }
if commands[args[0]] != nil { if commands[args[0]] != nil {
err := l.CallByParam(lua.P{ luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs))
Fn: commands[args[0]],
NRet: 1,
Protect: true,
}, luacmdArgs)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
"Error in command:\n\n" + err.Error())
return interp.NewExitStatus(1) return interp.NewExitStatus(1)
} }
luaexitcode := l.Get(-1)
var exitcode uint8 var exitcode uint8
l.Pop(1) if code, ok := luaexitcode.TryInt(); ok {
if code, ok := luaexitcode.(lua.LNumber); luaexitcode != lua.LNil && ok {
exitcode = uint8(code) exitcode = uint8(code)
} else if luaexitcode != rt.NilValue {
// deregister commander
delete(commands, args[0])
fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
} }
cmdFinish(exitcode, argstring, old)
return interp.NewExitStatus(exitcode) return interp.NewExitStatus(exitcode)
} }
err := lookpath(args[0]) err := lookpath(args[0])
if err == os.ErrPermission { if err == errNotExec {
hooks.Em.Emit("command.no-perm", args[0]) hooks.Em.Emit("command.no-perm", args[0])
hooks.Em.Emit("command.not-executable", args[0])
return interp.NewExitStatus(126) return interp.NewExitStatus(126)
} else if err != nil { } else if err != nil {
hooks.Em.Emit("command.not-found", args[0]) hooks.Em.Emit("command.not-found", args[0])
return interp.NewExitStatus(127) return interp.NewExitStatus(127)
} }
return interp.DefaultExecHandler(2 * time.Second)(ctx, args) killTimeout := 2 * time.Second
} // from here is basically copy-paste of the default exec handler from
runner, _ := interp.New( // sh/interp but with our job handling
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), hc := interp.HandlerCtx(ctx)
interp.ExecHandler(exechandle), path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
) if err != nil {
err = runner.Run(context.TODO(), file) fmt.Fprintln(hc.Stderr, err)
return interp.NewExitStatus(127)
}
return err env := hc.Env
envList := make([]string, 0, 64)
env.Each(func(name string, vr expand.Variable) bool {
if !vr.IsSet() {
// If a variable is set globally but unset in the
// runner, we need to ensure it's not part of the final
// list. Seems like zeroing the element is enough.
// This is a linear search, but this scenario should be
// rare, and the number of variables shouldn't be large.
for i, kv := range envList {
if strings.HasPrefix(kv, name+"=") {
envList[i] = ""
}
}
}
if vr.Exported && vr.Kind == expand.String {
envList = append(envList, name+"="+vr.String())
}
return true
})
cmd := exec.Cmd{
Path: path,
Args: args,
Env: envList,
Dir: hc.Dir,
Stdin: hc.Stdin,
Stdout: hc.Stdout,
Stderr: hc.Stderr,
}
err = cmd.Start()
var j *job
if bg {
j = jobs.getLatest()
j.setHandle(cmd.Process)
}
if err == nil {
if bg {
j.start(cmd.Process.Pid)
}
if done := ctx.Done(); done != nil {
go func() {
<-done
if killTimeout <= 0 || runtime.GOOS == "windows" {
cmd.Process.Signal(os.Kill)
return
}
// TODO: don't temporarily leak this goroutine
// if the program stops itself with the
// interrupt.
go func() {
time.Sleep(killTimeout)
cmd.Process.Signal(os.Kill)
}()
cmd.Process.Signal(os.Interrupt)
}()
}
err = cmd.Wait()
}
var exit uint8
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return ctx.Err()
}
exit = uint8(128 + status.Signal())
goto end
}
exit = uint8(status.ExitStatus())
goto end
}
exit = 1
goto end
case *exec.Error:
// did not start
fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
goto end
case nil:
goto end
default:
return err
}
end:
if bg {
j.exitCode = int(exit)
j.finish()
}
return interp.NewExitStatus(exit)
}
} }
// custom lookpath function so we know if a command is found *and* has execute permission func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable
func lookpath(file string) error { var skip []string
skip := []string{"./", "/", "../", "~/"} if runtime.GOOS == "windows" {
skip = []string{"./", "../", "~/", "C:"}
} else {
skip = []string{"./", "/", "../", "~/"}
}
for _, s := range skip { for _, s := range skip {
if strings.HasPrefix(file, s) { if strings.HasPrefix(file, s) {
err := findExecutable(file) return findExecutable(file, false, false)
return err
} }
} }
for _, dir := range filepath.SplitList(os.Getenv("PATH")) { for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
path := filepath.Join(dir, file) path := filepath.Join(dir, file)
err := findExecutable(path) err := findExecutable(path, true, false)
if err == os.ErrPermission { if err == errNotExec {
return err return err
} else if err == nil { } else if err == nil {
return nil return nil
@ -178,17 +399,6 @@ func lookpath(file string) error {
return os.ErrNotExist return os.ErrNotExist
} }
func findExecutable(name string) error {
f, err := os.Stat(name)
if err != nil {
return err
}
if m := f.Mode(); !m.IsDir() && m & 0111 != 0 {
return nil
}
return os.ErrPermission
}
func splitInput(input string) ([]string, string) { func splitInput(input string) ([]string, string) {
// end my suffering // end my suffering
// TODO: refactor this garbage // TODO: refactor this garbage
@ -242,11 +452,14 @@ func splitInput(input string) ([]string, string) {
return cmdArgs, cmdstr.String() return cmdArgs, cmdstr.String()
} }
func cmdFinish(code uint8, cmdstr, oldInput string) { func cmdFinish(code uint8, cmdstr string, private bool) {
// if input has space at the beginning, dont put in history // if input has space at the beginning, dont put in history
if interactive && !strings.HasPrefix(oldInput, " ") { if interactive && !private {
handleHistory(strings.TrimSpace(oldInput)) handleHistory(cmdstr)
} }
util.SetField(l, hshMod, "exitCode", lua.LNumber(code), "Exit code of last exected command") util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)), "Exit code of last exected command")
hooks.Em.Emit("command.exit", code, cmdstr) // using AsValue (to convert to lua type) on an interface which is an int
// results in it being unknown in lua .... ????
// so we allow the hook handler to take lua runtime Values
hooks.Em.Emit("command.exit", rt.IntValue(int64(code)), cmdstr)
} }

24
execfile_unix.go 100644
View File

@ -0,0 +1,24 @@
// +build linux darwin
package main
import (
"os"
)
func findExecutable(path string, inPath, dirs bool) error {
f, err := os.Stat(path)
if err != nil {
return err
}
if dirs {
if m := f.Mode(); m & 0111 != 0 {
return nil
}
} else {
if m := f.Mode(); !m.IsDir() && m & 0111 != 0 {
return nil
}
}
return errNotExec
}

View File

@ -0,0 +1,37 @@
// +build windows
package main
import (
"path/filepath"
"os"
)
func findExecutable(path string, inPath, dirs bool) error {
nameExt := filepath.Ext(path)
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
if inPath {
if nameExt == "" {
for _, ext := range pathExts {
_, err := os.Stat(path + ext)
if err == nil {
return nil
}
}
} else {
_, err := os.Stat(path)
if err == nil {
if contains(pathExts, nameExt) { return nil }
return errNotExec
}
}
} else {
_, err := os.Stat(path)
if err == nil {
if contains(pathExts, nameExt) { return nil }
return errNotExec
}
}
return os.ErrNotExist
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 99 KiB

19
go.mod
View File

@ -1,21 +1,32 @@
module hilbish module hilbish
go 1.16 go 1.17
require ( require (
github.com/arnodel/golua v0.0.0-20220221163911-dfcf252b6f86
github.com/blackfireio/osinfo v1.0.3 github.com/blackfireio/osinfo v1.0.3
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9
github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036 github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036
github.com/pborman/getopt v1.1.0 github.com/pborman/getopt v1.1.0
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
layeh.com/gopher-luar v1.0.10
mvdan.cc/sh/v3 v3.4.3 mvdan.cc/sh/v3 v3.4.3
) )
require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/arnodel/strftime v0.1.6 // indirect
github.com/evilsocket/islazy v1.10.6 // indirect
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
golang.org/x/text v0.3.6 // indirect
)
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e
replace github.com/maxlandon/readline => ./readline replace github.com/maxlandon/readline => ./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
replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac

41
go.sum
View File

@ -1,31 +1,24 @@
github.com/Rosettea/readline-1 v0.0.0-20220302012429-9ce5d23760f7 h1:LoY+kBKqMQqBcilRpVvifBTVve84asa3btpx3D/+IvM= github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac h1:dtXrgjch8PQyf7C90anZUquB5U3dr8AcMGJofeuirrI=
github.com/Rosettea/readline-1 v0.0.0-20220302012429-9ce5d23760f7/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs= github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
github.com/Rosettea/readline-1 v0.0.0-20220305004552-071c22768119 h1:rGsc30WTD5hk+oiXrAKsAIwZn5qBeTAdr29y3HhJh9E=
github.com/Rosettea/readline-1 v0.0.0-20220305004552-071c22768119/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93 h1:SmOkAEm3O7si8CURZSsSN0ZxCQ8IGiiulw8LMZ1V1Yc=
github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20211207003625-341c7985ad7d h1:KBttN41h/tPahmpaZavviwQ8q4rCkt5CD0HdVmfgPVA=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20211207003625-341c7985ad7d/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20220228022904-61f5e4493011 h1:+a61iNamZiO3Xru+l/1qtpKqqltVfWEm2r/rxH9hXxY=
github.com/Rosettea/readline-1 v0.1.0-beta.0.20220228022904-61f5e4493011/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5 h1:ygwVRX8gf5MHA0VzSgOdscCEoAJLjM8joEotfQPgAd0=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e h1:P2XupP8SaylWaudD1DqbWtZ3mIa8OsE9635LmR+Q+lg= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e h1:P2XupP8SaylWaudD1DqbWtZ3mIa8OsE9635LmR+Q+lg=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/arnodel/edit v0.0.0-20220202110212-dfc8d7a13890/go.mod h1:AcpttpuZBaL9xl8/CX+Em4fBTUbwIkJ66RiAsJlNrBk=
github.com/arnodel/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw=
github.com/arnodel/strftime v0.1.6/go.mod h1:5NbK5XqYK8QpRZpqKNt4OlxLtIB8cotkLk4KTKzJfWs=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/blackfireio/osinfo v1.0.3 h1:Yk2t2GTPjBcESv6nDSWZKO87bGMQgO+Hi9OoXPpxX8c= github.com/blackfireio/osinfo v1.0.3 h1:Yk2t2GTPjBcESv6nDSWZKO87bGMQgO+Hi9OoXPpxX8c=
github.com/blackfireio/osinfo v1.0.3/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA= github.com/blackfireio/osinfo v1.0.3/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA=
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 h1:xz6Nv3zcwO2Lila35hcb0QloCQsc38Al13RNEzWRpX4= github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 h1:xz6Nv3zcwO2Lila35hcb0QloCQsc38Al13RNEzWRpX4=
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9/go.mod h1:2wSM9zJkl1UQEFZgSd68NfCgRz1VL1jzy/RjCg+ULrs= github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9/go.mod h1:2wSM9zJkl1UQEFZgSd68NfCgRz1VL1jzy/RjCg+ULrs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo= github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo=
github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw= github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -35,34 +28,36 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/layeh/gopher-luar v1.0.10 h1:8NIv4MX1Arz96kK4buGK1D87DyDxKZyq6KKvJ2diHp0= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/layeh/gopher-luar v1.0.10/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw=
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0=
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0= github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0=

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"hilbish/util" "hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/chuckpreslar/emission" "github.com/chuckpreslar/emission"
"github.com/yuin/gopher-lua"
"layeh.com/gopher-luar"
) )
type Bait struct{ type Bait struct{
Em *emission.Emitter Em *emission.Emitter
Loader packagelib.Loader
} }
func New() Bait { func New() Bait {
@ -19,15 +20,27 @@ func New() Bait {
emitter.Off(hookname, hookfunc) emitter.Off(hookname, hookfunc)
fmt.Println(err) fmt.Println(err)
}) })
return Bait{ b := Bait{
Em: emitter, Em: emitter,
} }
b.Loader = packagelib.Loader{
Load: b.loaderFunc,
Name: "bait",
}
return b
} }
func (b *Bait) Loader(L *lua.LState) int { func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{}) exports := map[string]util.LuaExport{
"catch": util.LuaExport{b.bcatch, 2, false},
"catchOnce": util.LuaExport{b.bcatchOnce, 2, false},
"throw": util.LuaExport{b.bthrow, 1, true},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
util.Document(L, mod, util.Document(mod,
`Bait is the event emitter for Hilbish. Why name it bait? `Bait is the event emitter for Hilbish. Why name it bait?
Because it throws hooks that you can catch (emits events Because it throws hooks that you can catch (emits events
that you can listen to) and because why not, fun naming that you can listen to) and because why not, fun naming
@ -36,35 +49,81 @@ in on hooks to know when certain things have happened,
like when you've changed directory, a command has like when you've changed directory, a command has
failed, etc. To find all available hooks, see doc hooks.`) failed, etc. To find all available hooks, see doc hooks.`)
L.SetField(mod, "throw", luar.New(L, b.bthrow)) return rt.TableValue(mod), nil
L.SetField(mod, "catch", luar.New(L, b.bcatch)) }
L.SetField(mod, "catchOnce", luar.New(L, b.bcatchOnce))
L.Push(mod) func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, args ...interface{}) {
funcVal := rt.FunctionValue(catcher)
return 1 var luaArgs []rt.Value
for _, arg := range args {
var luarg rt.Value
switch arg.(type) {
case rt.Value: luarg = arg.(rt.Value)
default: luarg = rt.AsValue(arg)
}
luaArgs = append(luaArgs, luarg)
}
_, err := rt.Call1(t, funcVal, luaArgs...)
if err != nil {
e := rt.NewError(rt.StringValue(err.Error()))
e = e.AddContext(c.Next(), 1)
// panicking here won't actually cause hilbish to panic and instead will
// print the error and remove the hook (look at emission recover from above)
panic(e)
}
} }
// throw(name, ...args) // throw(name, ...args)
// Throws a hook with `name` with the provided `args` // Throws a hook with `name` with the provided `args`
// --- @param name string // --- @param name string
// --- @vararg any // --- @vararg any
func (b *Bait) bthrow(name string, args ...interface{}) { func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
b.Em.Emit(name, args...) if err := c.Check1Arg(); err != nil {
return nil, err
}
name, err := c.StringArg(0)
if err != nil {
return nil, err
}
ifaceSlice := make([]interface{}, len(c.Etc()))
for i, v := range c.Etc() {
ifaceSlice[i] = v
}
b.Em.Emit(name, ifaceSlice...)
return c.Next(), nil
} }
// catch(name, cb) // catch(name, cb)
// Catches a hook with `name`. Runs the `cb` when it is thrown // Catches a hook with `name`. Runs the `cb` when it is thrown
// --- @param name string // --- @param name string
// --- @param cb function // --- @param cb function
func (b *Bait) bcatch(name string, catcher func(...interface{})) { func (b *Bait) bcatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
b.Em.On(name, catcher) name, catcher, err := util.HandleStrCallback(t, c)
if err != nil {
return nil, err
}
b.Em.On(name, func(args ...interface{}) {
handleHook(t, c, name, catcher, args...)
})
return c.Next(), nil
} }
// catchOnce(name, cb) // catchOnce(name, cb)
// Same as catch, but only runs the `cb` once and then removes the hook // Same as catch, but only runs the `cb` once and then removes the hook
// --- @param name string // --- @param name string
// --- @param cb function // --- @param cb function
func (b *Bait) bcatchOnce(name string, catcher func(...interface{})) { func (b *Bait) bcatchOnce(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
b.Em.Once(name, catcher) name, catcher, err := util.HandleStrCallback(t, c)
if err != nil {
return nil, err
}
b.Em.Once(name, func(args ...interface{}) {
handleHook(t, c, name, catcher, args...)
})
return c.Next(), nil
} }

View File

@ -3,52 +3,68 @@ package commander
import ( import (
"hilbish/util" "hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/chuckpreslar/emission" "github.com/chuckpreslar/emission"
"github.com/yuin/gopher-lua"
) )
type Commander struct{ type Commander struct{
Events *emission.Emitter Events *emission.Emitter
Loader packagelib.Loader
} }
func New() Commander { func New() Commander {
return Commander{ c := Commander{
Events: emission.NewEmitter(), Events: emission.NewEmitter(),
} }
c.Loader = packagelib.Loader{
Load: c.loaderFunc,
Name: "commander",
}
return c
} }
func (c *Commander) Loader(L *lua.LState) int { func (c *Commander) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]lua.LGFunction{ exports := map[string]util.LuaExport{
"register": c.cregister, "register": util.LuaExport{c.cregister, 2, false},
"deregister": c.cderegister, "deregister": util.LuaExport{c.cderegister, 1, false},
} }
mod := L.SetFuncs(L.NewTable(), exports) mod := rt.NewTable()
util.Document(L, mod, "Commander is Hilbish's custom command library, a way to write commands in Lua.") util.SetExports(rtm, mod, exports)
L.Push(mod) util.Document(mod, "Commander is Hilbish's custom command library, a way to write commands in Lua.")
return 1 return rt.TableValue(mod), nil
} }
// register(name, cb) // register(name, cb)
// Register a command with `name` that runs `cb` when ran // Register a command with `name` that runs `cb` when ran
// --- @param name string // --- @param name string
// --- @param cb function // --- @param cb function
func (c *Commander) cregister(L *lua.LState) int { func (c *Commander) cregister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) {
cmdName := L.CheckString(1) cmdName, cmd, err := util.HandleStrCallback(t, ct)
cmd := L.CheckFunction(2) if err != nil {
return nil, err
}
c.Events.Emit("commandRegister", cmdName, cmd) c.Events.Emit("commandRegister", cmdName, cmd)
return 0 return ct.Next(), err
} }
// deregister(name) // deregister(name)
// Deregisters any command registered with `name` // Deregisters any command registered with `name`
// --- @param name string // --- @param name string
func (c *Commander) cderegister(L *lua.LState) int { func (c *Commander) cderegister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) {
cmdName := L.CheckString(1) if err := ct.Check1Arg(); err != nil {
return nil, err
}
cmdName, err := ct.StringArg(0)
if err != nil {
return nil, err
}
c.Events.Emit("commandDeregister", cmdName) c.Events.Emit("commandDeregister", cmdName)
return 0 return ct.Next(), err
} }

View File

@ -1,5 +1,3 @@
// The fs module provides easy and simple access to filesystem functions and other
// things, and acts an addition to the Lua standard library's I/O and fs functions.
package fs package fs
import ( import (
@ -8,51 +6,70 @@ import (
"strings" "strings"
"hilbish/util" "hilbish/util"
"github.com/yuin/gopher-lua"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
) )
func Loader(L *lua.LState) int { var Loader = packagelib.Loader{
mod := L.SetFuncs(L.NewTable(), exports) Load: loaderFunc,
Name: "fs",
}
util.Document(L, mod, `The fs module provides easy and simple access to func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{
"cd": util.LuaExport{fcd, 1, false},
"mkdir": util.LuaExport{fmkdir, 2, false},
"stat": util.LuaExport{fstat, 1, false},
"readdir": util.LuaExport{freaddir, 1, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
util.Document(mod, `The fs module provides easy and simple access to
filesystem functions and other things, and acts an filesystem functions and other things, and acts an
addition to the Lua standard library's I/O and fs functions.`) addition to the Lua standard library's I/O and fs functions.`)
L.Push(mod) return rt.TableValue(mod), nil
return 1
}
var exports = map[string]lua.LGFunction{
"cd": fcd,
"mkdir": fmkdir,
"stat": fstat,
"readdir": freaddir,
} }
// cd(dir) // cd(dir)
// Changes directory to `dir` // Changes directory to `dir`
// --- @param dir string // --- @param dir string
func fcd(L *lua.LState) int { func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
path := L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
err := os.Chdir(strings.TrimSpace(path)) }
path, err := c.StringArg(0)
if err != nil { if err != nil {
e := err.(*os.PathError).Err.Error() return nil, err
L.RaiseError(e + ": " + path)
} }
return 0 err = os.Chdir(strings.TrimSpace(path))
if err != nil {
return nil, err
}
return c.Next(), err
} }
// mkdir(name, recursive) // mkdir(name, recursive)
// Makes a directory called `name`. If `recursive` is true, it will create its parent directories. // Makes a directory called `name`. If `recursive` is true, it will create its parent directories.
// --- @param name string // --- @param name string
// --- @param recursive boolean // --- @param recursive boolean
func fmkdir(L *lua.LState) int { func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
dirname := L.CheckString(1) if err := c.CheckNArgs(2); err != nil {
recursive := L.ToBool(2) return nil, err
}
dirname, err := c.StringArg(0)
if err != nil {
return nil, err
}
recursive, err := c.BoolArg(1)
if err != nil {
return nil, err
}
path := strings.TrimSpace(dirname) path := strings.TrimSpace(dirname)
var err error
if recursive { if recursive {
err = os.MkdirAll(path, 0744) err = os.MkdirAll(path, 0744)
@ -60,51 +77,58 @@ func fmkdir(L *lua.LState) int {
err = os.Mkdir(path, 0744) err = os.Mkdir(path, 0744)
} }
if err != nil { if err != nil {
L.RaiseError(err.Error() + ": " + path) return nil, err
} }
return 0 return c.Next(), err
} }
// stat(path) // stat(path)
// Returns info about `path` // Returns info about `path`
// --- @param path string // --- @param path string
func fstat(L *lua.LState) int { func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
path := L.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
path, err := c.StringArg(0)
if err != nil {
return nil, err
}
pathinfo, err := os.Stat(path) pathinfo, err := os.Stat(path)
if err != nil { if err != nil {
L.RaiseError(err.Error() + ": " + path) return nil, err
return 0
} }
statTbl := L.NewTable() statTbl := rt.NewTable()
L.SetField(statTbl, "name", lua.LString(pathinfo.Name())) statTbl.Set(rt.StringValue("name"), rt.StringValue(pathinfo.Name()))
L.SetField(statTbl, "size", lua.LNumber(pathinfo.Size())) statTbl.Set(rt.StringValue("size"), rt.IntValue(pathinfo.Size()))
L.SetField(statTbl, "mode", lua.LString("0" + strconv.FormatInt(int64(pathinfo.Mode().Perm()), 8))) statTbl.Set(rt.StringValue("mode"), rt.StringValue("0" + strconv.FormatInt(int64(pathinfo.Mode().Perm()), 8)))
L.SetField(statTbl, "isDir", lua.LBool(pathinfo.IsDir())) statTbl.Set(rt.StringValue("isDir"), rt.BoolValue(pathinfo.IsDir()))
L.Push(statTbl)
return 1 return c.PushingNext1(t.Runtime, rt.TableValue(statTbl)), nil
} }
// readdir(dir) // readdir(dir)
// Returns a table of files in `dir` // Returns a table of files in `dir`
// --- @param dir string // --- @param dir string
// --- @return table // --- @return table
func freaddir(L *lua.LState) int { func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
dir := L.CheckString(1) if err := c.Check1Arg(); err != nil {
names := L.NewTable() return nil, err
}
dir, err := c.StringArg(0)
if err != nil {
return nil, err
}
names := rt.NewTable()
dirEntries, err := os.ReadDir(dir) dirEntries, err := os.ReadDir(dir)
if err != nil { if err != nil {
L.RaiseError(err.Error() + ": " + dir) return nil, err
return 0
} }
for _, entry := range dirEntries { for i, entry := range dirEntries {
names.Append(lua.LString(entry.Name())) names.Set(rt.IntValue(int64(i + 1)), rt.StringValue(entry.Name()))
} }
L.Push(names) return c.PushingNext1(t.Runtime, rt.TableValue(names)), nil
return 1
} }

View File

@ -5,76 +5,78 @@ import (
"hilbish/util" "hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"golang.org/x/term" "golang.org/x/term"
"github.com/yuin/gopher-lua"
) )
var termState *term.State var termState *term.State
var Loader = packagelib.Loader{
func Loader(L *lua.LState) int { Load: loaderFunc,
mod := L.SetFuncs(L.NewTable(), exports) Name: "terminal",
util.Document(L, mod, "The terminal library is a simple and lower level library for certain terminal interactions.")
L.Push(mod)
return 1
} }
var exports = map[string]lua.LGFunction{ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
"setRaw": termraw, exports := map[string]util.LuaExport{
"restoreState": termrestoreState, "setRaw": util.LuaExport{termsetRaw, 0, false},
"size": termsize, "restoreState": util.LuaExport{termrestoreState, 0, false},
"saveState": termsaveState, "size": util.LuaExport{termsize, 0, false},
"saveState": util.LuaExport{termsaveState, 0, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
util.Document(mod, "The terminal library is a simple and lower level library for certain terminal interactions.")
return rt.TableValue(mod), nil
} }
// size() // size()
// Gets the dimensions of the terminal. Returns a table with `width` and `height` // Gets the dimensions of the terminal. Returns a table with `width` and `height`
// Note: this is not the size in relation to the dimensions of the display // Note: this is not the size in relation to the dimensions of the display
func termsize(L *lua.LState) int { func termsize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
w, h, err := term.GetSize(int(os.Stdin.Fd())) w, h, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil { if err != nil {
L.RaiseError(err.Error()) return nil, err
return 0
} }
dimensions := L.NewTable()
L.SetField(dimensions, "width", lua.LNumber(w))
L.SetField(dimensions, "height", lua.LNumber(h))
L.Push(dimensions) dimensions := rt.NewTable()
return 1 dimensions.Set(rt.StringValue("width"), rt.IntValue(int64(w)))
dimensions.Set(rt.StringValue("height"), rt.IntValue(int64(h)))
return c.PushingNext1(t.Runtime, rt.TableValue(dimensions)), nil
} }
// saveState() // saveState()
// Saves the current state of the terminal // Saves the current state of the terminal
func termsaveState(L *lua.LState) int { func termsaveState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
state, err := term.GetState(int(os.Stdin.Fd())) state, err := term.GetState(int(os.Stdin.Fd()))
if err != nil { if err != nil {
L.RaiseError(err.Error()) return nil, err
return 0
} }
termState = state termState = state
return 0 return c.Next(), nil
} }
// restoreState() // restoreState()
// Restores the last saved state of the terminal // Restores the last saved state of the terminal
func termrestoreState(L *lua.LState) int { func termrestoreState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := term.Restore(int(os.Stdin.Fd()), termState) err := term.Restore(int(os.Stdin.Fd()), termState)
if err != nil { if err != nil {
L.RaiseError(err.Error()) return nil, err
} }
return 0 return c.Next(), nil
} }
// setRaw() // setRaw()
// Puts the terminal in raw mode // Puts the terminal in raw mode
func termraw(L *lua.LState) int { func termsetRaw(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
_, err := term.MakeRaw(int(os.Stdin.Fd())) _, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil { if err != nil {
L.RaiseError(err.Error()) return nil, err
} }
return 0 return c.Next(), nil
} }

View File

@ -78,3 +78,9 @@ func (h *fileHistory) Len() int {
func (h *fileHistory) Dump() interface{} { func (h *fileHistory) Dump() interface{} {
return h.items return h.items
} }
func (h *fileHistory) clear() {
h.items = []string{}
h.f.Truncate(0)
h.f.Sync()
}

142
job.go 100644
View File

@ -0,0 +1,142 @@
package main
import (
"sync"
"os"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var jobs *jobHandler
type job struct {
cmd string
running bool
id int
pid int
exitCode int
proc *os.Process
}
func (j *job) start(pid int) {
j.pid = pid
j.running = true
hooks.Em.Emit("job.start", j.lua())
}
func (j *job) stop() {
// finish will be called in exec handle
j.proc.Kill()
}
func (j *job) finish() {
j.running = false
hooks.Em.Emit("job.done", j.lua())
}
func (j *job) setHandle(handle *os.Process) {
j.proc = handle
}
func (j *job) lua() rt.Value {
jobFuncs := map[string]util.LuaExport{
"stop": {j.luaStop, 0, false},
}
luaJob := rt.NewTable()
util.SetExports(l, luaJob, jobFuncs)
luaJob.Set(rt.StringValue("cmd"), rt.StringValue(j.cmd))
luaJob.Set(rt.StringValue("running"), rt.BoolValue(j.running))
luaJob.Set(rt.StringValue("id"), rt.IntValue(int64(j.id)))
luaJob.Set(rt.StringValue("pid"), rt.IntValue(int64(j.pid)))
luaJob.Set(rt.StringValue("exitCode"), rt.IntValue(int64(j.exitCode)))
return rt.TableValue(luaJob)
}
func (j *job) luaStop(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if j.running {
j.stop()
}
return c.Next(), nil
}
type jobHandler struct {
jobs map[int]*job
latestID int
mu *sync.RWMutex
}
func newJobHandler() *jobHandler {
return &jobHandler{
jobs: make(map[int]*job),
latestID: 0,
mu: &sync.RWMutex{},
}
}
func (j *jobHandler) add(cmd string) {
j.mu.Lock()
defer j.mu.Unlock()
j.latestID++
j.jobs[j.latestID] = &job{
cmd: cmd,
running: false,
id: j.latestID,
}
}
func (j *jobHandler) getLatest() *job {
j.mu.RLock()
defer j.mu.RUnlock()
return j.jobs[j.latestID]
}
func (j *jobHandler) loader(rtm *rt.Runtime) *rt.Table {
jobFuncs := map[string]util.LuaExport{
"all": {j.luaAllJobs, 0, false},
"get": {j.luaGetJob, 1, false},
}
luaJob := rt.NewTable()
util.SetExports(rtm, luaJob, jobFuncs)
return luaJob
}
func (j *jobHandler) luaGetJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
j.mu.RLock()
defer j.mu.RUnlock()
if err := c.Check1Arg(); err != nil {
return nil, err
}
jobID, err := c.IntArg(0)
if err != nil {
return nil, err
}
job := j.jobs[int(jobID)]
if job == nil {
return c.Next(), nil
}
return c.PushingNext1(t.Runtime, job.lua()), nil
}
func (j *jobHandler) luaAllJobs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
j.mu.RLock()
defer j.mu.RUnlock()
jobTbl := rt.NewTable()
for id, job := range j.jobs {
jobTbl.Set(rt.IntValue(int64(id)), job.lua())
}
return c.PushingNext1(t.Runtime, rt.TableValue(jobTbl)), nil
}

View File

@ -89,21 +89,25 @@ end
ansikit.print = function(text) ansikit.print = function(text)
io.write(ansikit.format(text)) io.write(ansikit.format(text))
io.flush()
return ansikit return ansikit
end end
ansikit.printCode = function(code, terminate) ansikit.printCode = function(code, terminate)
io.write(ansikit.getCode(code, terminate)) io.write(ansikit.getCode(code, terminate))
io.flush()
return ansikit return ansikit
end end
ansikit.printCSI = function(code, endc) ansikit.printCSI = function(code, endc)
io.write(ansikit.getCSI(code, endc)) io.write(ansikit.getCSI(code, endc))
io.flush()
return ansikit return ansikit
end end
ansikit.println = function(text) ansikit.println = function(text)
print(ansikit.print(text)) io.write(ansikit.format(text) .. "\n")
io.flush()
return ansikit return ansikit
end end

@ -1 +1 @@
Subproject commit 5a59d0f4543eb982593750c52f7393e2fd2d15f9 Subproject commit b362397a83e4516415c809c7d690b52e79a95f6e

47
lua.go
View File

@ -4,40 +4,42 @@ import (
"fmt" "fmt"
"os" "os"
"hilbish/util"
"hilbish/golibs/bait" "hilbish/golibs/bait"
"hilbish/golibs/commander" "hilbish/golibs/commander"
"hilbish/golibs/fs" "hilbish/golibs/fs"
"hilbish/golibs/terminal" "hilbish/golibs/terminal"
"github.com/yuin/gopher-lua" rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib"
) )
var minimalconf = `hilbish.prompt '& '` var minimalconf = `hilbish.prompt '& '`
func luaInit() { func luaInit() {
l = lua.NewState() l = rt.New(os.Stdout)
l.OpenLibs() lib.LoadAll(l)
lib.LoadLibs(l, hilbishLoader)
// yes this is stupid, i know // yes this is stupid, i know
l.PreloadModule("hilbish", hilbishLoader) util.DoString(l, "hilbish = require 'hilbish'")
l.DoString("hilbish = require 'hilbish'")
// Add fs and terminal module module to Lua // Add fs and terminal module module to Lua
l.PreloadModule("fs", fs.Loader) lib.LoadLibs(l, fs.Loader)
l.PreloadModule("terminal", terminal.Loader) lib.LoadLibs(l, terminal.Loader)
cmds := commander.New() cmds := commander.New()
// When a command from Lua is added, register it for use // When a command from Lua is added, register it for use
cmds.Events.On("commandRegister", func(cmdName string, cmd *lua.LFunction) { cmds.Events.On("commandRegister", func(cmdName string, cmd *rt.Closure) {
commands[cmdName] = cmd commands[cmdName] = cmd
}) })
cmds.Events.On("commandDeregister", func(cmdName string) { cmds.Events.On("commandDeregister", func(cmdName string) {
delete(commands, cmdName) delete(commands, cmdName)
}) })
l.PreloadModule("commander", cmds.Loader) lib.LoadLibs(l, cmds.Loader)
hooks = bait.New() hooks = bait.New()
l.PreloadModule("bait", hooks.Loader) lib.LoadLibs(l, hooks.Loader)
// Add Ctrl-C handler // Add Ctrl-C handler
hooks.Em.On("signal.sigint", func() { hooks.Em.On("signal.sigint", func() {
@ -46,29 +48,28 @@ func luaInit() {
} }
}) })
l.SetGlobal("complete", l.NewFunction(hlcomplete))
// Add more paths that Lua can require from // Add more paths that Lua can require from
l.DoString("package.path = package.path .. " + requirePaths) err := util.DoString(l, "package.path = package.path .. " + requirePaths)
err := l.DoFile("prelude/init.lua")
if err != nil { if err != nil {
err = l.DoFile(preloadPath) fmt.Fprintln(os.Stderr, "Could not add preload paths! Libraries will be missing. This shouldn't happen.")
}
err = util.DoFile(l, "prelude/init.lua")
if err != nil {
err = util.DoFile(l, preloadPath)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, fmt.Fprintln(os.Stderr, "Missing preload file, builtins may be missing.")
"Missing preload file, builtins may be missing.")
} }
} }
} }
func runConfig(confpath string) { func runConfig(confpath string) {
if !interactive { if !interactive {
return return
} }
err := l.DoFile(confpath) err := util.DoFile(l, confpath)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err, fmt.Fprintln(os.Stderr, err, "\nAn error has occured while loading your config! Falling back to minimal default config.")
"\nAn error has occured while loading your config! Falling back to minimal default config.") util.DoString(l, minimalconf)
l.DoString(minimalconf)
} }
} }

62
main.go
View File

@ -10,20 +10,21 @@ import (
"runtime" "runtime"
"strings" "strings"
"hilbish/util"
"hilbish/golibs/bait" "hilbish/golibs/bait"
rt "github.com/arnodel/golua/runtime"
"github.com/pborman/getopt" "github.com/pborman/getopt"
"github.com/yuin/gopher-lua"
"github.com/maxlandon/readline" "github.com/maxlandon/readline"
"golang.org/x/term" "golang.org/x/term"
) )
var ( var (
l *lua.LState l *rt.Runtime
lr *lineReader lr *lineReader
commands = map[string]*lua.LFunction{} commands = map[string]*rt.Closure{}
luaCompletions = map[string]*lua.LFunction{} luaCompletions = map[string]*rt.Closure{}
confDir string confDir string
userDataDir string userDataDir string
@ -43,7 +44,7 @@ func main() {
// i honestly dont know what directories to use for this // i honestly dont know what directories to use for this
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux", "darwin":
userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir + "/.local/share") userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir + "/.local/share")
default: default:
// this is fine on windows, dont know about others // this is fine on windows, dont know about others
@ -55,7 +56,7 @@ func main() {
defaultConfDir = filepath.Join(confDir, "hilbish") defaultConfDir = filepath.Join(confDir, "hilbish")
} else { } else {
// else do ~ substitution // else do ~ substitution
defaultConfDir = expandHome(defaultHistDir) defaultConfDir = filepath.Join(expandHome(defaultConfDir), "hilbish")
} }
defaultConfPath = filepath.Join(defaultConfDir, "init.lua") defaultConfPath = filepath.Join(defaultConfDir, "init.lua")
if defaultHistDir == "" { if defaultHistDir == "" {
@ -142,27 +143,28 @@ func main() {
scanner := bufio.NewScanner(bufio.NewReader(os.Stdin)) scanner := bufio.NewScanner(bufio.NewReader(os.Stdin))
for scanner.Scan() { for scanner.Scan() {
text := scanner.Text() text := scanner.Text()
runInput(text, text) runInput(text, true)
} }
exit(0)
} }
if *cmdflag != "" { if *cmdflag != "" {
runInput(*cmdflag, *cmdflag) runInput(*cmdflag, true)
} }
if getopt.NArgs() > 0 { if getopt.NArgs() > 0 {
luaArgs := l.NewTable() luaArgs := rt.NewTable()
for _, arg := range getopt.Args() { for i, arg := range getopt.Args() {
luaArgs.Append(lua.LString(arg)) luaArgs.Set(rt.IntValue(int64(i)), rt.StringValue(arg))
} }
l.SetGlobal("args", luaArgs) l.GlobalEnv().Set(rt.StringValue("args"), rt.TableValue(luaArgs))
err := l.DoFile(getopt.Arg(0)) err := util.DoFile(l, getopt.Arg(0))
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) exit(1)
} }
os.Exit(0) exit(0)
} }
initialized = true initialized = true
@ -185,7 +187,10 @@ input:
fmt.Println("^C") fmt.Println("^C")
continue continue
} }
oldInput := input var priv bool
if strings.HasPrefix(input, " ") {
priv = true
}
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
if len(input) == 0 { if len(input) == 0 {
@ -198,6 +203,8 @@ input:
for { for {
input, err = continuePrompt(input) input, err = continuePrompt(input)
if err != nil { if err != nil {
running = true
lr.SetPrompt(fmtPrompt(prompt))
goto input // continue inside nested loop goto input // continue inside nested loop
} }
if !strings.HasSuffix(input, "\\") { if !strings.HasSuffix(input, "\\") {
@ -206,7 +213,7 @@ input:
} }
} }
runInput(input, oldInput) runInput(input, priv)
termwidth, _, err := term.GetSize(0) termwidth, _, err := term.GetSize(0)
if err != nil { if err != nil {
@ -268,8 +275,7 @@ func handleHistory(cmd string) {
func expandHome(path string) string { func expandHome(path string) string {
homedir := curuser.HomeDir homedir := curuser.HomeDir
return strings.Replace(path, "~", homedir, 1)
return strings.Replace(defaultHistDir, "~", homedir, 1)
} }
func removeDupes(slice []string) []string { func removeDupes(slice []string) []string {
@ -284,3 +290,21 @@ func removeDupes(slice []string) []string {
return newSlice return newSlice
} }
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func exit(code int) {
// wait for all timers to finish before exiting
for {
if timers.running == 0 {
os.Exit(code)
}
}
}

View File

@ -8,7 +8,7 @@ local _ = require 'succulent' -- Function additions
local oldDir = hilbish.cwd() local oldDir = hilbish.cwd()
local shlvl = tonumber(os.getenv 'SHLVL') local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then os.setenv('SHLVL', shlvl + 1) else os.setenv('SHLVL', 0) end if shlvl ~= nil then os.setenv('SHLVL', tostring(shlvl + 1)) else os.setenv('SHLVL', '0') end
-- Builtins -- Builtins
local recentDirs = {} local recentDirs = {}
@ -168,6 +168,9 @@ hilbish.userDir.config .. '/hilbish/init.lua' ..
and also change all global functions (prompt, alias) to be and also change all global functions (prompt, alias) to be
in the hilbish module (hilbish.prompt, hilbish.alias as examples). in the hilbish module (hilbish.prompt, hilbish.alias as examples).
And if this is your first time (most likely), you can copy a config
from ]] .. hilbish.dataDir,
[[
Since 1.0 is a big release, you'll want to check the changelog Since 1.0 is a big release, you'll want to check the changelog
at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0 at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0
to find more breaking changes. to find more breaking changes.
@ -214,14 +217,6 @@ do
end end
end, end,
}) })
bait.catch('command.exit', function ()
for key, value in pairs(virt_G) do
if type(value) == 'string' then
virt_G[key] = os.getenv(key)
end
end
end)
end end
commander.register('cdr', function(args) commander.register('cdr', function(args)
@ -263,7 +258,7 @@ bait.catch('command.not-found', function(cmd)
print(string.format('hilbish: %s not found', cmd)) print(string.format('hilbish: %s not found', cmd))
end) end)
bait.catch('command.no-perm', function(cmd) bait.catch('command.not-executable', function(cmd)
print(string.format('hilbish: %s: no permission', cmd)) print(string.format('hilbish: %s: not executable', cmd))
end) end)

View File

@ -34,32 +34,39 @@ const (
charCtrlHat // ^^ charCtrlHat // ^^
charCtrlUnderscore // ^_ charCtrlUnderscore // ^_
charBackspace2 = 127 // ASCII 1963 charBackspace2 = 127 // ASCII 1963
) )
// Escape sequences // Escape sequences
var ( var (
seqUp = string([]byte{27, 91, 65}) seqUp = string([]byte{27, 91, 65})
seqDown = string([]byte{27, 91, 66}) seqDown = string([]byte{27, 91, 66})
seqForwards = string([]byte{27, 91, 67}) seqForwards = string([]byte{27, 91, 67})
seqBackwards = string([]byte{27, 91, 68}) seqBackwards = string([]byte{27, 91, 68})
seqHome = string([]byte{27, 91, 72}) seqHome = string([]byte{27, 91, 72})
seqHomeSc = string([]byte{27, 91, 49, 126}) seqHomeSc = string([]byte{27, 91, 49, 126})
seqEnd = string([]byte{27, 91, 70}) seqEnd = string([]byte{27, 91, 70})
seqEndSc = string([]byte{27, 91, 52, 126}) seqEndSc = string([]byte{27, 91, 52, 126})
seqDelete = string([]byte{27, 91, 51, 126}) seqDelete = string([]byte{27, 91, 51, 126})
seqShiftTab = string([]byte{27, 91, 90}) seqDelete2 = string([]byte{27, 91, 80})
seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^[" seqCtrlDelete = string([]byte{27, 91, 51, 59, 53, 126})
seqAltR = string([]byte{27, 114}) // Used for alternative history seqCtrlDelete2 = string([]byte{27, 91, 77})
seqAltDelete = string([]byte{27, 91, 51, 59, 51, 126})
seqShiftTab = string([]byte{27, 91, 90})
seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^["
seqAltB = string([]byte{27, 98})
seqAltD = string([]byte{27, 100})
seqAltF = string([]byte{27, 102})
seqAltR = string([]byte{27, 114}) // Used for alternative history
seqAltBackspace = string([]byte{27, 127})
) )
const ( const (
seqPosSave = "\x1b[s" seqPosSave = "\x1b[s"
seqPosRestore = "\x1b[u" seqPosRestore = "\x1b[u"
seqClearLineAfer = "\x1b[0k" seqClearLineAfer = "\x1b[0K"
seqClearLineBefore = "\x1b[1k" seqClearLineBefore = "\x1b[1K"
seqClearLine = "\x1b[2k" seqClearLine = "\x1b[2K"
seqClearScreenBelow = "\x1b[0J" seqClearScreenBelow = "\x1b[0J"
seqClearScreen = "\x1b[2J" // Clears screen fully seqClearScreen = "\x1b[2J" // Clears screen fully
seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left
@ -78,6 +85,7 @@ const (
seqBold = "\x1b[1m" seqBold = "\x1b[1m"
seqUnderscore = "\x1b[4m" seqUnderscore = "\x1b[4m"
seqBlink = "\x1b[5m" seqBlink = "\x1b[5m"
seqInvert = "\x1b[7m"
) )
// Text colours // Text colours

View File

@ -121,7 +121,7 @@ func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) {
} }
if (x == g.tcPosX && y == g.tcPosY) && (g.isCurrent) { if (x == g.tcPosX && y == g.tcPosY) && (g.isCurrent) {
comp += seqCtermFg255 + seqFgBlackBright comp += seqInvert
} }
comp += fmt.Sprintf("%-"+cellWidth+"s %s", g.Suggestions[i], seqReset) comp += fmt.Sprintf("%-"+cellWidth+"s %s", g.Suggestions[i], seqReset)

View File

@ -188,7 +188,7 @@ func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
// function highlights the cell depending on current selector place. // function highlights the cell depending on current selector place.
highlight := func(y int, x int) string { highlight := func(y int, x int) string {
if y == g.tcPosY && x == g.tcPosX && g.isCurrent { if y == g.tcPosY && x == g.tcPosX && g.isCurrent {
return seqCtermFg255 + seqFgBlackBright return seqInvert
} }
return "" return ""
} }

View File

@ -101,7 +101,7 @@ func (g *CompletionGroup) writeMap(rl *Instance) (comp string) {
// Highlighting function // Highlighting function
highlight := func(y int) string { highlight := func(y int) string {
if y == g.tcPosY && g.isCurrent { if y == g.tcPosY && g.isCurrent {
return seqCtermFg255 + seqFgBlackBright return seqInvert
} }
return "" return ""
} }

View File

@ -100,12 +100,12 @@ func moveCursorBackwards(i int) {
printf("\x1b[%dD", i) printf("\x1b[%dD", i)
} }
func (rl *Instance) backspace() { func (rl *Instance) backspace(forward bool) {
if len(rl.line) == 0 || rl.pos == 0 { if len(rl.line) == 0 || rl.pos == 0 {
return return
} }
rl.deleteBackspace() rl.deleteBackspace(forward)
} }
func (rl *Instance) moveCursorByAdjust(adjust int) { func (rl *Instance) moveCursorByAdjust(adjust int) {

View File

@ -7,7 +7,7 @@ type EventReturn struct {
ForwardKey bool ForwardKey bool
ClearHelpers bool ClearHelpers bool
CloseReadline bool CloseReadline bool
HintText []rune InfoText []rune
NewLine []rune NewLine []rune
NewPos int NewPos int
} }

View File

@ -4,10 +4,12 @@ import "regexp"
// SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders // SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders
// them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed. // them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed.
/*
func (rl *Instance) SetHintText(s string) { func (rl *Instance) SetHintText(s string) {
rl.hintText = []rune(s) rl.hintText = []rune(s)
rl.renderHelpers() rl.renderHelpers()
} }
*/
func (rl *Instance) getHintText() { func (rl *Instance) getHintText() {
@ -27,7 +29,7 @@ func (rl *Instance) getHintText() {
// writeHintText - only writes the hint text and computes its offsets. // writeHintText - only writes the hint text and computes its offsets.
func (rl *Instance) writeHintText() { func (rl *Instance) writeHintText() {
if len(rl.hintText) == 0 { if len(rl.hintText) == 0 {
rl.hintY = 0 //rl.hintY = 0
return return
} }
@ -41,16 +43,16 @@ func (rl *Instance) writeHintText() {
wrapped, hintLen := WrapText(string(rl.hintText), width) wrapped, hintLen := WrapText(string(rl.hintText), width)
offset += hintLen offset += hintLen
rl.hintY = offset // rl.hintY = offset
hintText := string(wrapped) hintText := string(wrapped)
if len(hintText) > 0 { if len(hintText) > 0 {
print("\r" + rl.HintFormatting + string(hintText) + seqReset) print(rl.HintFormatting + string(hintText) + seqReset)
} }
} }
func (rl *Instance) resetHintText() { func (rl *Instance) resetHintText() {
rl.hintY = 0 //rl.hintY = 0
rl.hintText = []rune{} rl.hintText = []rune{}
} }

View File

@ -183,13 +183,13 @@ func (rl *Instance) completeHistory() (hist []*CompletionGroup) {
return return
} }
history = rl.altHistory history = rl.altHistory
rl.histHint = []rune(rl.altHistName + ": ") rl.histInfo = []rune(rl.altHistName + ": ")
} else { } else {
if rl.mainHistory == nil { if rl.mainHistory == nil {
return return
} }
history = rl.mainHistory history = rl.mainHistory
rl.histHint = []rune(rl.mainHistName + ": ") rl.histInfo = []rune(rl.mainHistName + ": ")
} }
hist[0].init(rl) hist[0].init(rl)

56
readline/info.go 100644
View File

@ -0,0 +1,56 @@
package readline
import "regexp"
// SetInfoText - a nasty function to force writing a new info text. It does not update helpers, it just renders
// them, so the info will survive until the helpers (thus including the info) will be updated/recomputed.
func (rl *Instance) SetInfoText(s string) {
rl.infoText = []rune(s)
rl.renderHelpers()
}
func (rl *Instance) getInfoText() {
if !rl.modeAutoFind && !rl.modeTabFind {
// Return if no infos provided by the user/engine
if rl.InfoText == nil {
rl.resetInfoText()
return
}
// The info text also works with the virtual completion line system.
// This way, the info is also refreshed depending on what we are pointing
// at with our cursor.
rl.infoText = rl.InfoText(rl.getCompletionLine())
}
}
// writeInfoText - only writes the info text and computes its offsets.
func (rl *Instance) writeInfoText() {
if len(rl.infoText) == 0 {
rl.infoY = 0
return
}
width := GetTermWidth()
// Wraps the line, and counts the number of newlines in the string,
// adjusting the offset as well.
re := regexp.MustCompile(`\r?\n`)
newlines := re.Split(string(rl.infoText), -1)
offset := len(newlines)
wrapped, infoLen := WrapText(string(rl.infoText), width)
offset += infoLen
rl.infoY = offset
infoText := string(wrapped)
if len(infoText) > 0 {
print("\r" + rl.InfoFormatting + string(infoText) + seqReset)
}
}
func (rl *Instance) resetInfoText() {
rl.infoY = 0
rl.infoText = []rune{}
}

View File

@ -30,11 +30,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
realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line. rightPrompt string
defaultPrompt []rune rightPromptLen int
promptLen int realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line.
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs defaultPrompt []rune
promptLen int
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs
// //
// Input Line --------------------------------------------------------------------------------- // Input Line ---------------------------------------------------------------------------------
@ -110,7 +112,7 @@ 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
mainHist bool // Which history stdin do we want mainHist bool // Which history stdin do we want
histHint []rune // We store a hist hint, for dual history sources histInfo []rune // We store a piece of hist info, for dual history sources
// //
// History ----------------------------------------------------------------------------------- // History -----------------------------------------------------------------------------------
@ -134,19 +136,33 @@ type Instance struct {
histNavIdx int // Used for quick history navigation. histNavIdx int // Used for quick history navigation.
// //
// Hints ------------------------------------------------------------------------------------- // Info -------------------------------------------------------------------------------------
// HintText is a helper function which displays hint text the prompt. // InfoText is a helper function which displays infio text below the prompt.
// HintText takes the line input from the promt and the cursor position. // InfoText takes the line input from the prompt and the cursor position.
// It returns the info text to display.
InfoText func([]rune, int) []rune
// InfoColor is any ANSI escape codes you wish to use for info formatting. By
// default this will just be blue.
InfoFormatting string
infoText []rune // The actual info text
infoY int // Offset to info, if it spans multiple lines
//
// Hints -----------------------------------------------------------------------------------
// HintText is a helper function which displays hint text right after the user's input.
// It takes the line input and cursor position.
// It returns the hint text to display. // It returns the hint text to display.
HintText func([]rune, int) []rune HintText func([]rune, int) []rune
// HintColor any ANSI escape codes you wish to use for hint formatting. By // HintFormatting is just a string to use as the formatting for the hint. By default
// default this will just be blue. // this will be a grey color.
HintFormatting string HintFormatting string
hintText []rune // The actual hint text hintText []rune
hintY int // Offset to hints, if it spans multiple lines
// //
// Vim Operatng Parameters ------------------------------------------------------------------- // Vim Operatng Parameters -------------------------------------------------------------------
@ -205,7 +221,8 @@ func NewInstance() *Instance {
rl.HistoryAutoWrite = true rl.HistoryAutoWrite = true
// Others // Others
rl.HintFormatting = seqFgBlue rl.InfoFormatting = seqFgBlue
rl.HintFormatting = "\x1b[2m"
rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn) rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn)
rl.TempDirectory = os.TempDir() rl.TempDirectory = os.TempDir()

View File

@ -57,9 +57,9 @@ func (rl *Instance) echo() {
// Print the input line with optional syntax highlighting // Print the input line with optional syntax highlighting
if rl.SyntaxHighlighter != nil { if rl.SyntaxHighlighter != nil {
print(rl.SyntaxHighlighter(line) + " ") print(rl.SyntaxHighlighter(line))
} else { } else {
print(string(line) + " ") print(string(line))
} }
} }
@ -125,14 +125,14 @@ func (rl *Instance) deleteX() {
rl.updateHelpers() rl.updateHelpers()
} }
func (rl *Instance) deleteBackspace() { func (rl *Instance) deleteBackspace(forward bool) {
switch { switch {
case len(rl.line) == 0: case len(rl.line) == 0:
return return
case rl.pos == 0: case forward:
rl.line = rl.line[1:] rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
case rl.pos > len(rl.line): case rl.pos > len(rl.line):
rl.backspace() // There is an infite loop going on here... rl.backspace(forward) // There is an infite loop going on here...
case rl.pos == len(rl.line): case rl.pos == len(rl.line):
rl.pos-- rl.pos--
rl.line = rl.line[:rl.pos] rl.line = rl.line[:rl.pos]
@ -176,3 +176,48 @@ func (rl *Instance) deleteToBeginning() {
rl.line = rl.line[rl.pos:] rl.line = rl.line[rl.pos:]
rl.pos = 0 rl.pos = 0
} }
func (rl *Instance) deleteToEnd() {
rl.resetVirtualComp(false)
// Keep everything before the cursor
rl.line = rl.line[:rl.pos]
}
// @TODO(Renzix): move to emacs sepecific file
func (rl *Instance) emacsForwardWord(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return
}
word := strings.TrimSpace(split[index])
switch {
case len(split) == 0:
return
case pos == len(word) && index != len(split)-1:
extrawhitespace := len(strings.TrimLeft(split[index], " ")) - len(word)
word = split[index+1]
adjust = len(word) + extrawhitespace
default:
adjust = len(word) - pos
}
return
}
func (rl *Instance) emacsBackwardWord(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return
}
switch {
case len(split) == 0:
return
case pos == 0 && index != 0:
adjust = len(split[index-1])
default:
adjust = pos
}
return
}

View File

@ -11,6 +11,13 @@ import (
// It also calculates the runes in the string as well as any non-printable escape codes. // It also calculates the runes in the string as well as any non-printable escape codes.
func (rl *Instance) SetPrompt(s string) { func (rl *Instance) SetPrompt(s string) {
rl.mainPrompt = s rl.mainPrompt = s
rl.computePrompt()
}
// SetRightPrompt sets the right prompt.
func (rl *Instance) SetRightPrompt(s string) {
rl.rightPrompt = s + " "
rl.computePrompt()
} }
// RefreshPromptLog - A simple function to print a string message (a log, or more broadly, // RefreshPromptLog - A simple function to print a string message (a log, or more broadly,
@ -20,7 +27,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
// We adjust cursor movement, depending on which mode we're currently in. // We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion { if !rl.modeTabCompletion {
rl.tcUsedY = 1 rl.tcUsedY = 1
// Account for the hint line // Account for the info line
} else if rl.modeTabCompletion && rl.modeAutoFind { } else if rl.modeTabCompletion && rl.modeAutoFind {
rl.tcUsedY = 0 rl.tcUsedY = 0
} else { } else {
@ -40,7 +47,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
moveCursorUp(1) moveCursorUp(1)
} }
rl.stillOnRefresh = true rl.stillOnRefresh = true
moveCursorUp(rl.hintY + rl.tcUsedY) moveCursorUp(rl.infoY + rl.tcUsedY)
moveCursorBackwards(GetTermWidth()) moveCursorBackwards(GetTermWidth())
print("\r\n" + seqClearScreenBelow) print("\r\n" + seqClearScreenBelow)
@ -68,12 +75,11 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) {
// RefreshPromptInPlace - Refreshes the prompt in the very same place he is. // RefreshPromptInPlace - Refreshes the prompt in the very same place he is.
func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) { func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// We adjust cursor movement, depending on which mode we're currently in. // We adjust cursor movement, depending on which mode we're currently in.
// Prompt data intependent // Prompt data intependent
if !rl.modeTabCompletion { if !rl.modeTabCompletion {
rl.tcUsedY = 1 rl.tcUsedY = 1
// Account for the hint line // Account for the info line
} else if rl.modeTabCompletion && rl.modeAutoFind { } else if rl.modeTabCompletion && rl.modeAutoFind {
rl.tcUsedY = 0 rl.tcUsedY = 0
} else { } else {
@ -82,7 +88,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// Update the prompt if a special has been passed. // Update the prompt if a special has been passed.
if prompt != "" { if prompt != "" {
rl.mainPrompt = prompt rl.SetPrompt(prompt)
} }
if rl.Multiline { if rl.Multiline {
@ -91,7 +97,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
// Clear the input line and everything below // Clear the input line and everything below
print(seqClearLine) print(seqClearLine)
moveCursorUp(rl.hintY + rl.tcUsedY) moveCursorUp(rl.infoY + rl.tcUsedY)
moveCursorBackwards(GetTermWidth()) moveCursorBackwards(GetTermWidth())
print("\r\n" + seqClearScreenBelow) print("\r\n" + seqClearScreenBelow)
@ -118,7 +124,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo
// We adjust cursor movement, depending on which mode we're currently in. // We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion { if !rl.modeTabCompletion {
rl.tcUsedY = 1 rl.tcUsedY = 1
} else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the hint line } else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the info line
rl.tcUsedY = 0 rl.tcUsedY = 0
} else { } else {
rl.tcUsedY = 1 rl.tcUsedY = 1
@ -137,7 +143,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo
// Update the prompt if a special has been passed. // Update the prompt if a special has been passed.
if prompt != "" { if prompt != "" {
rl.mainPrompt = prompt rl.SetPrompt(prompt)
} }
// Add a new line if needed // Add a new line if needed
@ -185,6 +191,7 @@ func (rl *Instance) computePrompt() (prompt []rune) {
// Strip color escapes // Strip color escapes
rl.promptLen = getRealLength(string(rl.realPrompt)) rl.promptLen = getRealLength(string(rl.realPrompt))
rl.rightPromptLen = getRealLength(string(rl.rightPrompt))
return return
} }
@ -205,3 +212,11 @@ func getRealLength(s string) (l int) {
stripped := ansi.Strip(s) stripped := ansi.Strip(s)
return uniseg.GraphemeClusterCount(stripped) return uniseg.GraphemeClusterCount(stripped)
} }
func (rl *Instance) echoRightPrompt() {
if rl.fullX < GetTermWidth() - rl.rightPromptLen - 1 {
moveCursorForwards(GetTermWidth())
moveCursorBackwards(rl.rightPromptLen)
print(rl.rightPrompt)
}
}

View File

@ -2,9 +2,11 @@ package readline
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"syscall"
) )
var rxMultiline = regexp.MustCompile(`[\r\n]+`) var rxMultiline = regexp.MustCompile(`[\r\n]+`)
@ -38,11 +40,12 @@ func (rl *Instance) Readline() (string, error) {
rl.modeViMode = VimInsert rl.modeViMode = VimInsert
rl.pos = 0 rl.pos = 0
rl.posY = 0 rl.posY = 0
rl.tcPrefix = ""
// Completion && hints init // Completion && infos init
rl.resetHintText() rl.resetInfoText()
rl.resetTabCompletion() rl.resetTabCompletion()
rl.getHintText() rl.getInfoText()
// History Init // History Init
// We need this set to the last command, so that we can access it quickly // We need this set to the last command, so that we can access it quickly
@ -62,7 +65,7 @@ func (rl *Instance) Readline() (string, error) {
return string(rl.line), nil return string(rl.line), nil
} }
// Finally, print any hints or completions // Finally, print any info or completions
// if the TabCompletion engines so desires // if the TabCompletion engines so desires
rl.renderHelpers() rl.renderHelpers()
@ -76,6 +79,12 @@ func (rl *Instance) Readline() (string, error) {
var err error var err error
i, err = os.Stdin.Read(b) i, err = os.Stdin.Read(b)
if err != nil { if err != nil {
if errors.Is(err, syscall.EAGAIN) {
err = syscall.SetNonblock(syscall.Stdin, false)
if err == nil {
continue
}
}
return "", err return "", err
} }
} }
@ -127,8 +136,8 @@ func (rl *Instance) Readline() (string, error) {
rl.updateHelpers() rl.updateHelpers()
} }
if len(ret.HintText) > 0 { if len(ret.InfoText) > 0 {
rl.hintText = ret.HintText rl.infoText = ret.InfoText
rl.clearHelpers() rl.clearHelpers()
rl.renderHelpers() rl.renderHelpers()
} }
@ -160,9 +169,18 @@ func (rl *Instance) Readline() (string, error) {
rl.clearHelpers() rl.clearHelpers()
return "", CtrlC return "", CtrlC
case charEOF: case charEOF: // ctrl d
rl.clearHelpers() if len(rl.line) == 0 {
return "", EOF rl.clearHelpers()
return "", EOF
}
if rl.modeTabFind {
rl.backspaceTabFind()
} else {
if (rl.pos < len(rl.line)) {
rl.deleteBackspace(true)
}
}
// Clear screen // Clear screen
case charCtrlL: case charCtrlL:
@ -173,8 +191,8 @@ func (rl *Instance) Readline() (string, error) {
} }
print(seqClearScreenBelow) print(seqClearScreenBelow)
rl.resetHintText() rl.resetInfoText()
rl.getHintText() rl.getInfoText()
rl.renderHelpers() rl.renderHelpers()
// Line Editing ------------------------------------------------------------------------------------ // Line Editing ------------------------------------------------------------------------------------
@ -188,6 +206,16 @@ func (rl *Instance) Readline() (string, error) {
rl.resetHelpers() rl.resetHelpers()
rl.updateHelpers() rl.updateHelpers()
case charCtrlK:
if rl.modeTabCompletion {
rl.resetVirtualComp(true)
}
// Delete everything after the cursor position
rl.saveBufToRegister(rl.line[rl.pos:])
rl.deleteToEnd()
rl.resetHelpers()
rl.updateHelpers()
case charBackspace, charBackspace2: case charBackspace, charBackspace2:
// When currently in history completion, we refresh and automatically // When currently in history completion, we refresh and automatically
// insert the first (filtered) candidate, virtually // insert the first (filtered) candidate, virtually
@ -213,7 +241,7 @@ func (rl *Instance) Readline() (string, error) {
// Vim mode has different behaviors // Vim mode has different behaviors
if rl.InputMode == Vim { if rl.InputMode == Vim {
if rl.modeViMode == VimInsert { if rl.modeViMode == VimInsert {
rl.backspace() rl.backspace(false)
} else if rl.pos != 0 { } else if rl.pos != 0 {
rl.pos-- rl.pos--
} }
@ -222,7 +250,7 @@ func (rl *Instance) Readline() (string, error) {
} }
// Else emacs deletes a character // Else emacs deletes a character
rl.backspace() rl.backspace(false)
rl.renderHelpers() rl.renderHelpers()
} }
@ -387,6 +415,10 @@ func (rl *Instance) Readline() (string, error) {
rl.renderHelpers() rl.renderHelpers()
} }
case charCtrlUnderscore:
rl.undoLast()
rl.viUndoSkipAppend = true
case '\r': case '\r':
fallthrough fallthrough
case '\n': case '\n':
@ -516,22 +548,27 @@ func (rl *Instance) editorInput(r []rune) {
case VimReplaceMany: case VimReplaceMany:
for _, char := range r { for _, char := range r {
rl.deleteX() if rl.pos != len(rl.line) {
rl.deleteX()
}
rl.insert([]rune{char}) rl.insert([]rune{char})
} }
rl.refreshVimStatus() rl.refreshVimStatus()
default: default:
// For some reason Ctrl+k messes with the input line, so ignore it. // Don't insert control keys
if r[0] == 11 { if r[0] >= 1 && r[0] <= 31 {
return return
} }
// We reset the history nav counter each time we come here: // We reset the history nav counter each time we come here:
// We don't need it when inserting text. // We don't need it when inserting text.
rl.histNavIdx = 0 rl.histNavIdx = 0
rl.insert(r) rl.insert(r)
rl.writeHintText()
} }
rl.echoRightPrompt()
if len(rl.multisplit) == 0 { if len(rl.multisplit) == 0 {
rl.syntaxCompletion() rl.syntaxCompletion()
} }
@ -625,6 +662,8 @@ func (rl *Instance) escapeSeq(r []rune) {
} }
rl.mainHist = true rl.mainHist = true
rl.walkHistory(1) rl.walkHistory(1)
moveCursorForwards(len(rl.line) - rl.pos)
rl.pos = len(rl.line)
case seqDown: case seqDown:
if rl.modeTabCompletion { if rl.modeTabCompletion {
@ -636,6 +675,8 @@ func (rl *Instance) escapeSeq(r []rune) {
} }
rl.mainHist = true rl.mainHist = true
rl.walkHistory(-1) rl.walkHistory(-1)
moveCursorForwards(len(rl.line) - rl.pos)
rl.pos = len(rl.line)
case seqForwards: case seqForwards:
if rl.modeTabCompletion { if rl.modeTabCompletion {
@ -647,8 +688,7 @@ func (rl *Instance) escapeSeq(r []rune) {
} }
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) || if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) { (rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
moveCursorForwards(1) rl.moveCursorByAdjust(1)
rl.pos++
} }
rl.updateHelpers() rl.updateHelpers()
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
@ -663,10 +703,7 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.renderHelpers() rl.renderHelpers()
return return
} }
if rl.pos > 0 { rl.moveCursorByAdjust(-1)
moveCursorBackwards(1)
rl.pos--
}
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
rl.updateHelpers() rl.updateHelpers()
@ -689,32 +726,64 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.updateHelpers() rl.updateHelpers()
return return
case seqCtrlRightArrow: case seqCtrlRightArrow:
rl.insert(rl.hintText)
rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine)) rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine))
rl.updateHelpers() rl.updateHelpers()
return return
case seqDelete: case seqDelete,seqDelete2:
if rl.modeTabFind { if rl.modeTabFind {
rl.backspaceTabFind() rl.backspaceTabFind()
} else { } else {
rl.deleteBackspace() if (rl.pos < len(rl.line)) {
rl.deleteBackspace(true)
}
} }
case seqHome, seqHomeSc: case seqHome, seqHomeSc:
if rl.modeTabCompletion { if rl.modeTabCompletion {
return return
} }
moveCursorBackwards(rl.pos) rl.moveCursorByAdjust(-rl.pos)
rl.pos = 0 rl.updateHelpers()
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
case seqEnd, seqEndSc: case seqEnd, seqEndSc:
if rl.modeTabCompletion { if rl.modeTabCompletion {
return return
} }
moveCursorForwards(len(rl.line) - rl.pos) rl.moveCursorByAdjust(len(rl.line) - rl.pos)
rl.pos = len(rl.line) rl.updateHelpers()
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
case seqAltB:
if rl.modeTabCompletion {
return
}
// This is only available in Insert mode
if rl.modeViMode != VimInsert {
return
}
move := rl.emacsBackwardWord(tokeniseLine)
rl.moveCursorByAdjust(-move)
rl.updateHelpers()
case seqAltF:
if rl.modeTabCompletion {
return
}
// This is only available in Insert mode
if rl.modeViMode != VimInsert {
return
}
move := rl.emacsForwardWord(tokeniseLine)
rl.moveCursorByAdjust(move)
rl.updateHelpers()
case seqAltR: case seqAltR:
rl.resetVirtualComp(false) rl.resetVirtualComp(false)
// For some modes only, if we are in vim Keys mode, // For some modes only, if we are in vim Keys mode,
@ -733,6 +802,36 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.updateTabFind([]rune{}) rl.updateTabFind([]rune{})
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true
case seqAltBackspace:
if rl.modeTabCompletion {
rl.resetVirtualComp(false)
}
// This is only available in Insert mode
if rl.modeViMode != VimInsert {
return
}
rl.saveToRegister(rl.viJumpB(tokeniseLine))
rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine))
rl.updateHelpers()
case seqCtrlDelete, seqCtrlDelete2, seqAltD:
if rl.modeTabCompletion {
rl.resetVirtualComp(false)
}
rl.saveToRegister(rl.emacsForwardWord(tokeniseLine))
// vi delete, emacs forward, funny huh
rl.viDeleteByAdjust(rl.emacsForwardWord(tokeniseLine))
rl.updateHelpers()
case seqAltDelete:
if rl.modeTabCompletion {
rl.resetVirtualComp(false)
}
rl.saveToRegister(-rl.emacsBackwardWord(tokeniseLine))
rl.viDeleteByAdjust(-rl.emacsBackwardWord(tokeniseLine))
rl.updateHelpers()
default: default:
if rl.modeTabFind { if rl.modeTabFind {
return return
@ -768,6 +867,8 @@ func (rl *Instance) escapeSeq(r []rune) {
} }
func (rl *Instance) carridgeReturn() { func (rl *Instance) carridgeReturn() {
rl.moveCursorByAdjust(len(rl.line))
rl.updateHelpers()
rl.clearHelpers() rl.clearHelpers()
print("\r\n") print("\r\n")
if rl.HistoryAutoWrite { if rl.HistoryAutoWrite {

View File

@ -259,9 +259,9 @@ func (r *registers) resetRegister() {
// The user can show registers completions and insert, no matter the cursor position. // The user can show registers completions and insert, no matter the cursor position.
func (rl *Instance) completeRegisters() (groups []*CompletionGroup) { func (rl *Instance) completeRegisters() (groups []*CompletionGroup) {
// We set the hint exceptionally // We set the info exceptionally
hint := BLUE + "-- registers --" + RESET info := BLUE + "-- registers --" + RESET
rl.hintText = []rune(hint) rl.infoText = []rune(info)
// Make the groups // Make the groups
anonRegs := &CompletionGroup{ anonRegs := &CompletionGroup{

View File

@ -93,19 +93,16 @@ func (rl *Instance) getTabSearchCompletion() {
} }
rl.getCurrentGroup() rl.getCurrentGroup()
// Set the hint for this completion mode // Set the info for this completion mode
rl.hintText = append([]rune("Completion search: "), rl.tfLine...) rl.infoText = append([]rune("Completion search: "), rl.tfLine...)
// Set the hint for this completion mode
rl.hintText = append([]rune("Completion search: "), rl.tfLine...)
for _, g := range rl.tcGroups { for _, g := range rl.tcGroups {
g.updateTabFind(rl) g.updateTabFind(rl)
} }
// If total number of matches is zero, we directly change the hint, and return // If total number of matches is zero, we directly change the info, and return
if comps, _, _ := rl.getCompletionCount(); comps == 0 { if comps, _, _ := rl.getCompletionCount(); comps == 0 {
rl.hintText = append(rl.hintText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) rl.infoText = append(rl.infoText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
} }
} }
@ -120,25 +117,25 @@ func (rl *Instance) getHistorySearchCompletion() {
rl.tcGroups = checkNilItems(rl.tcGroups) // Avoid nil maps in groups rl.tcGroups = checkNilItems(rl.tcGroups) // Avoid nil maps in groups
rl.getCurrentGroup() // Make sure there is a current group rl.getCurrentGroup() // Make sure there is a current group
// The history hint is already set, but overwrite it if we don't have completions // The history info is already set, but overwrite it if we don't have completions
if len(rl.tcGroups[0].Suggestions) == 0 { if len(rl.tcGroups[0].Suggestions) == 0 {
rl.histHint = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED, rl.histInfo = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED,
"No command history source, or empty (Ctrl-G/Esc to cancel)", RESET)) "No command history source, or empty (Ctrl-G/Esc to cancel)", RESET))
rl.hintText = rl.histHint rl.infoText = rl.histInfo
return return
} }
// Set the hint line with everything // Set the info line with everything
rl.histHint = append([]rune("\033[38;5;183m"+string(rl.histHint)+RESET), rl.tfLine...) rl.histInfo = append([]rune("\033[38;5;183m"+string(rl.histInfo)+RESET), rl.tfLine...)
rl.histHint = append(rl.histHint, []rune(RESET)...) rl.histInfo = append(rl.histInfo, []rune(RESET)...)
rl.hintText = rl.histHint rl.infoText = rl.histInfo
// Refresh filtered candidates // Refresh filtered candidates
rl.tcGroups[0].updateTabFind(rl) rl.tcGroups[0].updateTabFind(rl)
// If no items matched history, add hint text that we failed to search // If no items matched history, add info text that we failed to search
if len(rl.tcGroups[0].Suggestions) == 0 { if len(rl.tcGroups[0].Suggestions) == 0 {
rl.hintText = append(rl.histHint, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) rl.infoText = append(rl.histInfo, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
return return
} }
} }
@ -301,15 +298,15 @@ func (rl *Instance) cropCompletions(comps string) (cropped string, usedY int) {
// Else we go on, but we have more comps than what allowed: // Else we go on, but we have more comps than what allowed:
// we will add a line to the end of the comps, giving the actualized // we will add a line to the end of the comps, giving the actualized
// number of completions remaining and not printed // number of completions remaining and not printed
var moreComps = func(cropped string, offset int) (hinted string, noHint bool) { var moreComps = func(cropped string, offset int) (infoed string, noInfo bool) {
_, _, adjusted := rl.getCompletionCount() _, _, adjusted := rl.getCompletionCount()
remain := adjusted - offset remain := adjusted - offset
if remain == 0 { if remain == 0 {
return cropped, true return cropped, true
} }
hint := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain) info := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain)
hinted = cropped + hint infoed = cropped + info
return hinted, false return infoed, false
} }
// Get the current absolute candidate position (prev groups x suggestions + curGroup.tcPosY) // Get the current absolute candidate position (prev groups x suggestions + curGroup.tcPosY)
@ -512,7 +509,7 @@ func (rl *Instance) hasOneCandidate() bool {
// - The terminal lengh // - The terminal lengh
// we use this function to prompt for confirmation before printing comps. // we use this function to prompt for confirmation before printing comps.
func (rl *Instance) promptCompletionConfirm(sentence string) { func (rl *Instance) promptCompletionConfirm(sentence string) {
rl.hintText = []rune(sentence) rl.infoText = []rune(sentence)
rl.compConfirmWait = true rl.compConfirmWait = true
rl.viUndoSkipAppend = true rl.viUndoSkipAppend = true

View File

@ -33,7 +33,7 @@ func (rl *Instance) updateTabFind(r []rune) {
var err error var err error
rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine)) rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine))
if err != nil { if err != nil {
rl.hintText = []rune(Red("Failed to match search regexp")) rl.infoText = []rune(Red("Failed to match search regexp"))
} }
// We update and print // We update and print

View File

@ -1,12 +1,15 @@
package readline package readline
import "golang.org/x/text/width"
// updateHelpers is a key part of the whole refresh process: // updateHelpers is a key part of the whole refresh process:
// it should coordinate reprinting the input line, any hints and completions // it should coordinate reprinting the input line, any Infos and completions
// and manage to get back to the current (computed) cursor coordinates // and manage to get back to the current (computed) cursor coordinates
func (rl *Instance) updateHelpers() { func (rl *Instance) updateHelpers() {
// Load all hints & completions before anything. // Load all Infos & completions before anything.
// Thus overwrites anything having been dirtily added/forced/modified, like rl.SetHintText() // Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText()
rl.getInfoText()
rl.getHintText() rl.getHintText()
if rl.modeTabCompletion { if rl.modeTabCompletion {
rl.getTabCompletion() rl.getTabCompletion()
@ -20,6 +23,23 @@ func (rl *Instance) updateHelpers() {
rl.renderHelpers() rl.renderHelpers()
} }
const tabWidth = 4
func getWidth(x []rune) int {
var w int
for _, j := range x {
k := width.LookupRune(j).Kind()
if j == '\t' {
w += tabWidth
} else if k == width.EastAsianWide || k == width.EastAsianFullwidth {
w += 2
} else {
w++
}
}
return w
}
// Update reference should be called only once in a "loop" (not Readline(), but key control loop) // Update reference should be called only once in a "loop" (not Readline(), but key control loop)
func (rl *Instance) updateReferences() { func (rl *Instance) updateReferences() {
@ -32,11 +52,11 @@ func (rl *Instance) updateReferences() {
var fullLine, cPosLine int var fullLine, cPosLine int
if len(rl.currentComp) > 0 { if len(rl.currentComp) > 0 {
fullLine = len(rl.lineComp) fullLine = getWidth(rl.lineComp)
cPosLine = len(rl.lineComp[:rl.pos]) cPosLine = getWidth(rl.lineComp[:rl.pos])
} else { } else {
fullLine = len(rl.line) fullLine = getWidth(rl.line)
cPosLine = len(rl.line[:rl.pos]) cPosLine = getWidth(rl.line[:rl.pos])
} }
// We need the X offset of the whole line // We need the X offset of the whole line
@ -46,6 +66,10 @@ func (rl *Instance) updateReferences() {
fullRest := toEndLine % GetTermWidth() fullRest := toEndLine % GetTermWidth()
rl.fullX = fullRest rl.fullX = fullRest
if fullRest == 0 && fullOffset > 0 {
print("\n")
}
// Use rl.pos value to get the offset to go TO/FROM the CURRENT POSITION // Use rl.pos value to get the offset to go TO/FROM the CURRENT POSITION
lineToCursorPos := rl.promptLen + cPosLine lineToCursorPos := rl.promptLen + cPosLine
offsetToCursor := lineToCursorPos / GetTermWidth() offsetToCursor := lineToCursorPos / GetTermWidth()
@ -75,11 +99,11 @@ func (rl *Instance) resetHelpers() {
rl.modeAutoFind = false rl.modeAutoFind = false
// Now reset all below-input helpers // Now reset all below-input helpers
rl.resetHintText() rl.resetInfoText()
rl.resetTabCompletion() rl.resetTabCompletion()
} }
// clearHelpers - Clears everything: prompt, input, hints & comps, // clearHelpers - Clears everything: prompt, input, Infos & comps,
// and comes back at the prompt. // and comes back at the prompt.
func (rl *Instance) clearHelpers() { func (rl *Instance) clearHelpers() {
@ -97,25 +121,42 @@ func (rl *Instance) clearHelpers() {
moveCursorForwards(rl.posX) moveCursorForwards(rl.posX)
} }
// renderHelpers - pritns all components (prompt, line, hints & comps) // renderHelpers - pritns all components (prompt, line, Infos & comps)
// and replaces the cursor to its current position. This function never // and replaces the cursor to its current position. This function never
// computes or refreshes any value, except from inside the echo function. // computes or refreshes any value, except from inside the echo function.
func (rl *Instance) renderHelpers() { func (rl *Instance) renderHelpers() {
// Optional, because neutral on placement // when the instance is in this state we want it to be "below" the user's
// input for it to be aligned properly
if !rl.compConfirmWait {
rl.writeHintText()
}
rl.echo() rl.echo()
if rl.modeTabCompletion {
// in tab complete mode we want it to update
// when something has been selected
// (dynamic!!)
rl.getHintText()
rl.writeHintText()
} else if !rl.compConfirmWait {
// for the same reason above of wanting it below user input, do nothing here
} else {
rl.writeHintText()
}
rl.echoRightPrompt()
// Go at beginning of first line after input remainder // Go at beginning of first line after input remainder
moveCursorDown(rl.fullY - rl.posY) moveCursorDown(rl.fullY - rl.posY)
moveCursorBackwards(GetTermWidth()) moveCursorBackwards(GetTermWidth())
// Print hints, check for any confirmation hint current. // Print Infos, check for any confirmation Info current.
// (do not overwrite the confirmation question hint) // (do not overwrite the confirmation question Info)
if !rl.compConfirmWait { if !rl.compConfirmWait {
if len(rl.hintText) > 0 { if len(rl.infoText) > 0 {
print("\n") print("\n")
} }
rl.writeHintText() rl.writeInfoText()
moveCursorBackwards(GetTermWidth()) moveCursorBackwards(GetTermWidth())
// Print completions and go back to beginning of this line // Print completions and go back to beginning of this line
@ -126,17 +167,17 @@ func (rl *Instance) renderHelpers() {
} }
// If we are still waiting for the user to confirm too long completions // If we are still waiting for the user to confirm too long completions
// Immediately refresh the hints // Immediately refresh the Infos
if rl.compConfirmWait { if rl.compConfirmWait {
print("\n") print("\n")
rl.writeHintText() rl.writeInfoText()
rl.getHintText() rl.getInfoText()
moveCursorBackwards(GetTermWidth()) moveCursorBackwards(GetTermWidth())
} }
// Anyway, compensate for hint printout // Anyway, compensate for Info printout
if len(rl.hintText) > 0 { if len(rl.infoText) > 0 {
moveCursorUp(rl.hintY) moveCursorUp(rl.infoY)
} else if !rl.compConfirmWait { } else if !rl.compConfirmWait {
moveCursorUp(1) moveCursorUp(1)
} else if rl.compConfirmWait { } else if rl.compConfirmWait {

View File

@ -399,22 +399,22 @@ func (rl *Instance) refreshVimStatus() {
rl.updateHelpers() rl.updateHelpers()
} }
// viHintMessage - lmorg's way of showing Vim status is to overwrite the hint. // viInfoMessage - lmorg's way of showing Vim status is to overwrite the info.
// Currently not used, as there is a possibility to show the current Vim mode in the prompt. // Currently not used, as there is a possibility to show the current Vim mode in the prompt.
func (rl *Instance) viHintMessage() { func (rl *Instance) viInfoMessage() {
switch rl.modeViMode { switch rl.modeViMode {
case VimKeys: case VimKeys:
rl.hintText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)") rl.infoText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
case VimInsert: case VimInsert:
rl.hintText = []rune("-- INSERT --") rl.infoText = []rune("-- INSERT --")
case VimReplaceOnce: case VimReplaceOnce:
rl.hintText = []rune("-- REPLACE CHARACTER --") rl.infoText = []rune("-- REPLACE CHARACTER --")
case VimReplaceMany: case VimReplaceMany:
rl.hintText = []rune("-- REPLACE --") rl.infoText = []rune("-- REPLACE --")
case VimDelete: case VimDelete:
rl.hintText = []rune("-- DELETE --") rl.infoText = []rune("-- DELETE --")
default: default:
rl.getHintText() rl.getInfoText()
} }
rl.clearHelpers() rl.clearHelpers()

View File

@ -33,7 +33,7 @@ func (rl *Instance) viDelete(r rune) {
rl.saveBufToRegister(rl.line) rl.saveBufToRegister(rl.line)
rl.clearLine() rl.clearLine()
rl.resetHelpers() rl.resetHelpers()
rl.getHintText() rl.getInfoText()
case 'e': case 'e':
vii := rl.getViIterations() vii := rl.getViIterations()

264
rl.go
View File

@ -5,23 +5,26 @@ import (
"io" "io"
"strings" "strings"
"hilbish/util"
"github.com/maxlandon/readline" "github.com/maxlandon/readline"
"github.com/yuin/gopher-lua" rt "github.com/arnodel/golua/runtime"
) )
type lineReader struct { type lineReader struct {
rl *readline.Instance rl *readline.Instance
} }
var fileHist *fileHistory var fileHist *fileHistory
var hinter *rt.Closure
var highlighter *rt.Closure
// other gophers might hate this naming but this is local, shut up
func newLineReader(prompt string, noHist bool) *lineReader { func newLineReader(prompt string, noHist bool) *lineReader {
rl := readline.NewInstance() rl := readline.NewInstance()
// we don't mind hilbish.read rl instances having completion, // we don't mind hilbish.read rl instances having completion,
// but it cant have shared history // but it cant have shared history
if !noHist { if !noHist {
fileHist = newFileHistory() fileHist = newFileHistory()
rl.SetHistoryCtrlR("file", fileHist) rl.SetHistoryCtrlR("History", fileHist)
rl.HistoryAutoWrite = false rl.HistoryAutoWrite = false
} }
rl.ShowVimMode = false rl.ShowVimMode = false
@ -44,9 +47,45 @@ func newLineReader(prompt string, noHist bool) *lineReader {
} }
hooks.Em.Emit("hilbish.vimAction", actionStr, args) hooks.Em.Emit("hilbish.vimAction", actionStr, args)
} }
rl.HintText = func(line []rune, pos int) []rune {
if hinter == nil {
return []rune{}
}
retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(highlighter),
rt.StringValue(string(line)), rt.IntValue(int64(pos)))
if err != nil {
fmt.Println(err)
return []rune{}
}
hintText := ""
if luaStr, ok := retVal.TryString(); ok {
hintText = luaStr
}
return []rune(hintText)
}
rl.SyntaxHighlighter = func(line []rune) string {
if highlighter == nil {
return string(line)
}
retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(highlighter),
rt.StringValue(string(line)))
if err != nil {
fmt.Println(err)
return string(line)
}
highlighted := ""
if luaStr, ok := retVal.TryString(); ok {
highlighted = luaStr
}
return highlighted
}
rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) { rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) {
ctx := string(line) ctx := string(line)
var completions []string
var compGroup []*readline.CompletionGroup var compGroup []*readline.CompletionGroup
@ -75,23 +114,20 @@ func newLineReader(prompt string, noHist bool) *lineReader {
return prefix, compGroup return prefix, compGroup
} else { } else {
if completecb, ok := luaCompletions["command." + fields[0]]; ok { if completecb, ok := luaCompletions["command." + fields[0]]; ok {
luaFields := l.NewTable() luaFields := rt.NewTable()
for _, f := range fields { for i, f := range fields {
luaFields.Append(lua.LString(f)) luaFields.Set(rt.IntValue(int64(i + 1)), rt.StringValue(f))
} }
err := l.CallByParam(lua.P{
Fn: completecb, // we must keep the holy 80 cols
NRet: 1, luacompleteTable, err := rt.Call1(l.MainThread(),
Protect: true, rt.FunctionValue(completecb), rt.StringValue(query),
}, lua.LString(query), lua.LString(ctx), luaFields) rt.StringValue(ctx), rt.TableValue(luaFields))
if err != nil { if err != nil {
return "", compGroup return "", compGroup
} }
luacompleteTable := l.Get(-1)
l.Pop(1)
/* /*
as an example with git, as an example with git,
completion table should be structured like: completion table should be structured like:
@ -116,60 +152,98 @@ func newLineReader(prompt string, noHist bool) *lineReader {
it is the responsibility of the completer it is the responsibility of the completer
to work on subcommands and subcompletions to work on subcommands and subcompletions
*/ */
if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok { if cmpTbl, ok := luacompleteTable.TryTable(); ok {
cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) { nextVal := rt.NilValue
if key.Type() == lua.LTNumber { for {
// completion group next, val, ok := cmpTbl.Next(nextVal)
if value.Type() == lua.LTTable { if next == rt.NilValue {
luaCmpGroup := value.(*lua.LTable) break
compType := luaCmpGroup.RawGet(lua.LString("type")) }
compItems := luaCmpGroup.RawGet(lua.LString("items")) nextVal = next
if compType.Type() != lua.LTString {
l.RaiseError("bad type name for completion (expected string, got %v)", compType.Type().String())
}
if compItems.Type() != lua.LTTable {
l.RaiseError("bad items for completion (expected table, got %v)", compItems.Type().String())
}
var items []string
itemDescriptions := make(map[string]string)
compItems.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) {
if k.Type() == lua.LTString {
// ['--flag'] = {'description', '--flag-alias'}
itm := v.(*lua.LTable)
items = append(items, k.String())
itemDescriptions[k.String()] = itm.RawGet(lua.LNumber(1)).String()
} else {
items = append(items, v.String())
}
})
var dispType readline.TabDisplayType _, ok = next.TryInt()
switch compType.String() { valTbl, okk := val.TryTable()
case "grid": dispType = readline.TabDisplayGrid if !ok || !okk {
case "list": dispType = readline.TabDisplayList // TODO: error?
// need special cases, will implement later break
//case "map": dispType = readline.TabDisplayMap }
luaCompType := valTbl.Get(rt.StringValue("type"))
luaCompItems := valTbl.Get(rt.StringValue("items"))
compType, ok := luaCompType.TryString()
compItems, okk := luaCompItems.TryTable()
if !ok || !okk {
// TODO: error
break
}
var items []string
itemDescriptions := make(map[string]string)
nxVal := rt.NilValue
for {
nx, vl, _ := compItems.Next(nxVal)
if nx == rt.NilValue {
break
}
nxVal = nx
if tstr := nx.Type(); tstr == rt.StringType {
// ['--flag'] = {'description', '--flag-alias'}
nxStr, ok := nx.TryString()
vlTbl, okk := vl.TryTable()
if !ok || !okk {
// TODO: error
continue
} }
compGroup = append(compGroup, &readline.CompletionGroup{ items = append(items, nxStr)
DisplayType: dispType, itemDescription, ok := vlTbl.Get(rt.IntValue(1)).TryString()
Descriptions: itemDescriptions, if !ok {
Suggestions: items, // TODO: error
TrimSlash: false, continue
NoSpace: true, }
}) itemDescriptions[nxStr] = itemDescription
} else if tstr == rt.IntType {
vlStr, okk := vl.TryString()
if !okk {
// TODO: error
continue
}
items = append(items, vlStr)
} else {
// TODO: error
continue
} }
} }
})
var dispType readline.TabDisplayType
switch compType {
case "grid": dispType = readline.TabDisplayGrid
case "list": dispType = readline.TabDisplayList
// need special cases, will implement later
//case "map": dispType = readline.TabDisplayMap
}
compGroup = append(compGroup, &readline.CompletionGroup{
DisplayType: dispType,
Descriptions: itemDescriptions,
Suggestions: items,
TrimSlash: false,
NoSpace: true,
})
}
} }
} }
if len(compGroup) == 0 { if len(compGroup) == 0 {
completions = fileComplete(query, ctx, fields) completions, p := fileComplete(query, ctx, fields)
compGroup = append(compGroup, &readline.CompletionGroup{ fcompGroup := []*readline.CompletionGroup{{
TrimSlash: false, TrimSlash: false,
NoSpace: true, NoSpace: true,
Suggestions: completions, Suggestions: completions,
}) }}
return p, fcompGroup
} }
} }
return "", compGroup return "", compGroup
@ -208,6 +282,13 @@ func (lr *lineReader) SetPrompt(p string) {
} }
} }
func (lr *lineReader) SetRightPrompt(p string) {
lr.rl.SetRightPrompt(p)
if initialized && !running {
lr.rl.RefreshPromptInPlace("")
}
}
func (lr *lineReader) AddHistory(cmd string) { func (lr *lineReader) AddHistory(cmd string) {
fileHist.Write(cmd) fileHist.Write(cmd)
} }
@ -221,56 +302,65 @@ func (lr *lineReader) Resize() {
} }
// lua module // lua module
func (lr *lineReader) Loader(L *lua.LState) *lua.LTable { func (lr *lineReader) Loader(rtm *rt.Runtime) *rt.Table {
lrLua := map[string]lua.LGFunction{ lrLua := map[string]util.LuaExport{
"add": lr.luaAddHistory, "add": {lr.luaAddHistory, 1, false},
"all": lr.luaAllHistory, "all": {lr.luaAllHistory, 0, false},
"clear": lr.luaClearHistory, "clear": {lr.luaClearHistory, 0, false},
"get": lr.luaGetHistory, "get": {lr.luaGetHistory, 1, false},
"size": lr.luaSize, "size": {lr.luaSize, 0, false},
} }
mod := l.SetFuncs(l.NewTable(), lrLua) mod := rt.NewTable()
util.SetExports(rtm, mod, lrLua)
return mod return mod
} }
func (lr *lineReader) luaAddHistory(l *lua.LState) int { func (lr *lineReader) luaAddHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
cmd := l.CheckString(1) if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
lr.AddHistory(cmd) lr.AddHistory(cmd)
return 0 return c.Next(), nil
} }
func (lr *lineReader) luaSize(L *lua.LState) int { func (lr *lineReader) luaSize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
L.Push(lua.LNumber(fileHist.Len())) return c.PushingNext1(t.Runtime, rt.IntValue(int64(fileHist.Len()))), nil
return 1
} }
func (lr *lineReader) luaGetHistory(L *lua.LState) int { func (lr *lineReader) luaGetHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
idx := L.CheckInt(1) if err := c.Check1Arg(); err != nil {
cmd, _ := fileHist.GetLine(idx) return nil, err
L.Push(lua.LString(cmd)) }
idx, err := c.IntArg(0)
if err != nil {
return nil, err
}
return 0 cmd, _ := fileHist.GetLine(int(idx))
return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil
} }
func (lr *lineReader) luaAllHistory(L *lua.LState) int { func (lr *lineReader) luaAllHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
tbl := L.NewTable() tbl := rt.NewTable()
size := fileHist.Len() size := fileHist.Len()
for i := 1; i < size; i++ { for i := 1; i < size; i++ {
cmd, _ := fileHist.GetLine(i) cmd, _ := fileHist.GetLine(i)
tbl.Append(lua.LString(cmd)) tbl.Set(rt.IntValue(int64(i)), rt.StringValue(cmd))
} }
L.Push(tbl) return c.PushingNext1(t.Runtime, rt.TableValue(tbl)), nil
return 0
} }
func (lr *lineReader) luaClearHistory(l *lua.LState) int { func (lr *lineReader) luaClearHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return 0 fileHist.clear()
return c.Next(), nil
} }

56
runnermode.go 100644
View File

@ -0,0 +1,56 @@
package main
import (
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
func runnerModeLoader(rtm *rt.Runtime) *rt.Table {
exports := map[string]util.LuaExport{
"sh": {shRunner, 1, false},
"lua": {luaRunner, 1, false},
"setMode": {hlrunnerMode, 1, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
return mod
}
func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
input, exitCode, err := handleSh(cmd)
var luaErr rt.Value = rt.NilValue
if err != nil {
luaErr = rt.StringValue(err.Error())
}
return c.PushingNext(t.Runtime, rt.StringValue(input), rt.IntValue(int64(exitCode)), luaErr), nil
}
func luaRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
input, exitCode, err := handleLua(cmd)
var luaErr rt.Value = rt.NilValue
if err != nil {
luaErr = rt.StringValue(err.Error())
}
return c.PushingNext(t.Runtime, rt.StringValue(input), rt.IntValue(int64(exitCode)), luaErr), nil
}

106
timer.go 100644
View File

@ -0,0 +1,106 @@
package main
import (
"errors"
"fmt"
"os"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
type timerType int64
const (
timerInterval timerType = iota
timerTimeout
)
type timer struct{
id int
typ timerType
running bool
dur time.Duration
fun *rt.Closure
th *timerHandler
ticker *time.Ticker
channel chan bool
}
func (t *timer) start() error {
if t.running {
return errors.New("timer is already running")
}
t.running = true
t.th.running++
t.ticker = time.NewTicker(t.dur)
go func() {
for {
select {
case <-t.ticker.C:
_, err := rt.Call1(l.MainThread(), rt.FunctionValue(t.fun))
if err != nil {
fmt.Fprintln(os.Stderr, "Error in function:\n", err)
t.stop()
}
// only run one for timeout
if t.typ == timerTimeout {
t.stop()
}
case <-t.channel:
t.ticker.Stop()
return
}
}
}()
return nil
}
func (t *timer) stop() error {
if !t.running {
return errors.New("timer not running")
}
t.channel <- true
t.running = false
t.th.running--
return nil
}
func (t *timer) luaStart(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := t.start()
if err != nil {
return nil, err
}
return c.Next(), nil
}
func (t *timer) luaStop(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
err := t.stop()
if err != nil {
return nil, err
}
return c.Next(), nil
}
func (t *timer) lua() rt.Value {
tExports := map[string]util.LuaExport{
"start": {t.luaStart, 0, false},
"stop": {t.luaStop, 0, false},
}
luaTimer := rt.NewTable()
util.SetExports(l, luaTimer, tExports)
luaTimer.Set(rt.StringValue("type"), rt.IntValue(int64(t.typ)))
luaTimer.Set(rt.StringValue("running"), rt.BoolValue(t.running))
luaTimer.Set(rt.StringValue("duration"), rt.IntValue(int64(t.dur / time.Millisecond)))
return rt.TableValue(luaTimer)
}

102
timerhandler.go 100644
View File

@ -0,0 +1,102 @@
package main
import (
"sync"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var timers *timerHandler
type timerHandler struct {
mu *sync.RWMutex
timers map[int]*timer
latestID int
running int
}
func newTimerHandler() *timerHandler {
return &timerHandler{
timers: make(map[int]*timer),
latestID: 0,
mu: &sync.RWMutex{},
}
}
func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure) *timer {
th.mu.Lock()
defer th.mu.Unlock()
th.latestID++
t := &timer{
typ: typ,
fun: fun,
dur: dur,
channel: make(chan bool, 1),
th: th,
id: th.latestID,
}
th.timers[th.latestID] = t
return t
}
func (th *timerHandler) get(id int) *timer {
th.mu.RLock()
defer th.mu.RUnlock()
return th.timers[id]
}
func (th *timerHandler) luaCreate(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(3); err != nil {
return nil, err
}
timerTypInt, err := c.IntArg(0)
if err != nil {
return nil, err
}
ms, err := c.IntArg(1)
if err != nil {
return nil, err
}
cb, err := c.ClosureArg(2)
if err != nil {
return nil, err
}
timerTyp := timerType(timerTypInt)
tmr := th.create(timerTyp, time.Duration(ms) * time.Millisecond, cb)
return c.PushingNext1(t.Runtime, tmr.lua()), nil
}
func (th *timerHandler) luaGet(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
id, err := c.IntArg(0)
if err != nil {
return nil, err
}
t := th.get(int(id))
if t != nil {
return c.PushingNext1(thr.Runtime, t.lua()), nil
}
return c.Next(), nil
}
func (th *timerHandler) loader(rtm *rt.Runtime) *rt.Table {
thExports := map[string]util.LuaExport{
"create": {th.luaCreate, 3, false},
"get": {th.luaGet, 1, false},
}
luaTh := rt.NewTable()
util.SetExports(rtm, luaTh, thExports)
return luaTh
}

19
util/export.go 100644
View File

@ -0,0 +1,19 @@
package util
import (
rt "github.com/arnodel/golua/runtime"
)
// LuaExport represents a Go function which can be exported to Lua.
type LuaExport struct {
Function rt.GoFunctionFunc
ArgNum int
Variadic bool
}
// SetExports puts the Lua function exports in the table.
func SetExports(rtm *rt.Runtime, tbl *rt.Table, exports map[string]LuaExport) {
for name, export := range exports {
rtm.SetEnvGoFunc(tbl, name, export.Function, export.ArgNum, export.Variadic)
}
}

View File

@ -1,32 +1,120 @@
package util package util
import "github.com/yuin/gopher-lua" import (
"bufio"
"io"
"os"
rt "github.com/arnodel/golua/runtime"
)
// Document adds a documentation string to a module. // Document adds a documentation string to a module.
// It is accessible via the __doc metatable. // It is accessible via the __doc metatable.
func Document(L *lua.LState, module lua.LValue, doc string) { func Document(module *rt.Table, doc string) {
mt := L.GetMetatable(module) mt := module.Metatable()
if mt == lua.LNil {
mt = L.NewTable() if mt == nil {
L.SetMetatable(module, mt) mt = rt.NewTable()
module.SetMetatable(mt)
} }
L.SetField(mt, "__doc", lua.LString(doc))
mt.Set(rt.StringValue("__doc"), rt.StringValue(doc))
} }
// SetField sets a field in a table, adding docs for it. // 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. // It is accessible via the __docProp metatable. It is a table of the names of the fields.
func SetField(L *lua.LState, module lua.LValue, field string, value lua.LValue, doc string) { func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value, doc string) {
mt := L.GetMetatable(module) // TODO: ^ rtm isnt needed, i should remove it
if mt == lua.LNil { mt := module.Metatable()
mt = L.NewTable()
docProp := L.NewTable()
L.SetField(mt, "__docProp", docProp)
L.SetMetatable(module, mt) if mt == nil {
mt = rt.NewTable()
docProp := rt.NewTable()
mt.Set(rt.StringValue("__docProp"), rt.TableValue(docProp))
module.SetMetatable(mt)
} }
docProp := L.GetTable(mt, lua.LString("__docProp")) docProp := mt.Get(rt.StringValue("__docProp"))
L.SetField(docProp, field, lua.LString(doc)) docProp.AsTable().Set(rt.StringValue(field), rt.StringValue(doc))
L.SetField(module, field, value) module.Set(rt.StringValue(field), value)
} }
// DoString runs the code string in the Lua runtime.
func DoString(rtm *rt.Runtime, code string) error {
chunk, err := rtm.CompileAndLoadLuaChunk("<string>", []byte(code), rt.TableValue(rtm.GlobalEnv()))
if chunk != nil {
_, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(chunk))
}
return err
}
// DoFile runs the contents of the file in the Lua runtime.
func DoFile(rtm *rt.Runtime, path string) error {
f, err := os.Open(path)
defer f.Close()
if err != nil {
return err
}
reader := bufio.NewReader(f)
c, err := reader.ReadByte()
if err != nil && err != io.EOF {
return err
}
// unread so a char won't be missing
err = reader.UnreadByte()
if err != nil {
return err
}
var buf []byte
if c == byte('#') {
// shebang - skip that line
_, err := reader.ReadBytes('\n')
if err != nil && err != io.EOF {
return err
}
buf = []byte{'\n'}
}
for {
line, err := reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
break
}
return err
}
buf = append(buf, line...)
}
clos, err := rtm.LoadFromSourceOrCode(path, buf, "bt", rt.TableValue(rtm.GlobalEnv()), false)
if clos != nil {
_, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(clos))
}
return err
}
// HandleStrCallback handles function parameters for Go functions which take
// a string and a closure.
func HandleStrCallback(t *rt.Thread, c *rt.GoCont) (string, *rt.Closure, error) {
if err := c.CheckNArgs(2); err != nil {
return "", nil, err
}
name, err := c.StringArg(0)
if err != nil {
return "", nil, err
}
cb, err := c.ClosureArg(1)
if err != nil {
return "", nil, err
}
return name, cb, err
}

View File

@ -2,8 +2,7 @@ package main
// String vars that are free to be changed at compile time // String vars that are free to be changed at compile time
var ( var (
version = "v1.0.4" version = "v2.0.0"
defaultConfDir = "" // ~ will be substituted for home, path for user's default config
defaultHistDir = "" defaultHistDir = ""
commonRequirePaths = "';./libs/?/init.lua;./?/init.lua;./?/?.lua'" commonRequirePaths = "';./libs/?/init.lua;./?/init.lua;./?/?.lua'"

View File

@ -17,4 +17,5 @@ var (
dataDir = "/usr/local/share/hilbish" dataDir = "/usr/local/share/hilbish"
preloadPath = dataDir + "/prelude/init.lua" preloadPath = dataDir + "/prelude/init.lua"
sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config
defaultConfDir = getenv("XDG_CONFIG_HOME", "~/.config")
) )

View File

@ -17,4 +17,5 @@ var (
dataDir = "/usr/share/hilbish" dataDir = "/usr/share/hilbish"
preloadPath = dataDir + "/prelude/init.lua" preloadPath = dataDir + "/prelude/init.lua"
sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config
defaultConfDir = ""
) )

View File

@ -11,4 +11,5 @@ var (
dataDir = "~\\Appdata\\Roaming\\Hilbish" // ~ and \ gonna cry? dataDir = "~\\Appdata\\Roaming\\Hilbish" // ~ and \ gonna cry?
preloadPath = dataDir + "\\prelude\\init.lua" preloadPath = dataDir + "\\prelude\\init.lua"
sampleConfPath = dataDir + "\\hilbishrc.lua" // Path to default/sample config sampleConfPath = dataDir + "\\hilbishrc.lua" // Path to default/sample config
defaultConfDir = ""
) )