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

Compare commits

..

No commits in common. "master" and "v2.3.1" have entirely different histories.

134 changed files with 4363 additions and 3604 deletions

View File

@ -9,19 +9,10 @@ jobs:
gen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- name: Download Task
run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d'
- name: Build
run: ./bin/task
- name: Run docgen (go-written)
- name: Run docgen
run: go run cmd/docgen/docgen.go
- name: Run docgen (lua-written)
run: ./hilbish cmd/docgen/docgen.lua
- name: Commit new docs
uses: stefanzweifel/git-auto-commit-action@v4
with:

View File

@ -1,55 +1,10 @@
# 🎀 Changelog
## Unreleased
### Added
- Forward/Right arrow key will fill in hint text (#327)
- The readline library adds the ability to create custom instances of the Hilbish
line editor. Now, `hilbish.editor` has been changed to a readline instance, instead of just being a table of a few functions to access it.
This means the colon operator is now the *preferred* way of accessing its functions,
and the dot operator will cause errors in 3.0.
Example: `hilbish.editor.getLine()` should be changed to `hilbish.editor:getLine()`
before 3.0
- Added the `hilbish.editor:read` and `hilbish.editor:log(text)` functions.
- `yarn` threading library (See the docs)
### Changed
- Documentation for Lunacolors has been improved, with more information added.
- Values returned by bait hooks will be passed to the `throw` caller
- `display` property to completion groups entries to style completion entries when type is `list`.
example:
```lua
local cg = {
items = {
'list item 1',
['--command-flag-here'] = {'this does a thing', '--the-flag-alias'},
['--styled-command-flag-here'] = {'this does a thing', '--the-flag-alias', display = lunacolors.blue '--styled-command-flag-here'}
},
type = 'list'
}
```
## [2.3.4] - 2024-12-28
### Fixed
- Skip over file and prevent panic if info cannot be retrieved during file completion (due to permission error or anything else)
- Apply environment variables properly after 2.3 shell interpreter changes
- hilbish.sink.readAll() function now reads data that doesn't end in a newline
## [2.3.3] - 2024-11-04
### Fixed
- Heredocs having issues
### Added
- Adding `\` at the end of input will add a newline and prompt for more input.
## [2.3.2] - 2024-07-30
### Fixed
- Command path searching due to 2.3 changes to the shell interpreter
## [2.3.1] - 2024-07-27
[hehe when you see it release](https://youtu.be/AaAF51Gwbxo?si=rhj2iYuQRkqDa693&t=64)
### Added
- `hilbish.opts.tips` was added to display random tips on start up.
- `nature.opts.tips` was added to display random tips on start up.
Displayed tips can be modified via the `hilbish.tips` table.
### Fixed
@ -814,9 +769,6 @@ This input for example will prompt for more input to complete:
First "stable" release of Hilbish.
[2.3.4]: https://github.com/Rosettea/Hilbish/compare/v2.3.3...v2.3.4
[2.3.3]: https://github.com/Rosettea/Hilbish/compare/v2.3.2...v2.3.3
[2.3.2]: https://github.com/Rosettea/Hilbish/compare/v2.3.1...v2.3.2
[2.3.1]: https://github.com/Rosettea/Hilbish/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/Rosettea/Hilbish/compare/v2.2.3...v2.3.0
[2.2.3]: https://github.com/Rosettea/Hilbish/compare/v2.2.2...v2.2.3

View File

@ -13,23 +13,19 @@
<br>
Hilbish is an extensible shell designed to be highly customizable.
It is configured in Lua, and provides a good range of features.
It aims to be easy to use for anyone, and powerful enough for
those who need more.
It is configured in Lua and provides a good range of features.
It aims to be easy to use for anyone but powerful enough for
those who need it.
The motivation for choosing Lua was that its simpler and better to use
than old shell scripts. It's fine for basic interactive shell uses,
and supports [both Lua and Sh interactively](https://rosettea.github.io/Hilbish/docs/features/runner-mode/).
That's the only place Hilbish can use traditional shell syntax though;
everything else is Lua and aims to be infinitely configurable.
If something isn't, open an issue!
than old shell script. It's fine for basic interactive shell uses,
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!
# Screenshots
<div align="center">
<img src="gallery/tab.png">
<img src="gallery/pillprompt.png">
</div>
# Getting Hilbish

View File

@ -6,7 +6,7 @@ vars:
PREFIX: '{{default "/usr/local" .PREFIX}}'
bindir__: '{{.PREFIX}}/bin'
BINDIR: '{{default .bindir__ .BINDIR}}'
libdir__: ''
libdir__: '{{.PREFIX}}/share/hilbish'
LIBDIR: '{{default .libdir__ .LIBDIR}}'
goflags__: '-ldflags "-s -w -X main.dataDir={{.LIBDIR}}"'
GOFLAGS: '{{default .goflags__ .GOFLAGS}}'

236
api.go
View File

@ -13,9 +13,10 @@
package main
import (
//"bytes"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"runtime"
@ -25,31 +26,32 @@ import (
"hilbish/util"
"github.com/arnodel/golua/lib/packagelib"
rt "github.com/arnodel/golua/runtime"
//"github.com/arnodel/golua/lib/iolib"
"github.com/arnodel/golua/lib/packagelib"
"github.com/arnodel/golua/lib/iolib"
"github.com/maxlandon/readline"
//"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/interp"
)
var exports = map[string]util.LuaExport{
"alias": {hlalias, 2, false},
"appendPath": {hlappendPath, 1, false},
"complete": {hlcomplete, 2, false},
"cwd": {hlcwd, 0, false},
"exec": {hlexec, 1, false},
"goro": {hlgoro, 1, true},
"alias": {hlalias, 2, false},
"appendPath": {hlappendPath, 1, false},
"complete": {hlcomplete, 2, false},
"cwd": {hlcwd, 0, false},
"exec": {hlexec, 1, false},
"runnerMode": {hlrunnerMode, 1, false},
"goro": {hlgoro, 1, true},
"highlighter": {hlhighlighter, 1, false},
"hinter": {hlhinter, 1, false},
"hinter": {hlhinter, 1, false},
"multiprompt": {hlmultiprompt, 1, false},
"prependPath": {hlprependPath, 1, false},
"prompt": {hlprompt, 1, true},
"inputMode": {hlinputMode, 1, false},
"interval": {hlinterval, 2, false},
"read": {hlread, 1, false},
"timeout": {hltimeout, 2, false},
"which": {hlwhich, 1, false},
"prompt": {hlprompt, 1, true},
"inputMode": {hlinputMode, 1, false},
"interval": {hlinterval, 2, false},
"read": {hlread, 1, false},
"run": {hlrun, 1, true},
"timeout": {hltimeout, 2, false},
"which": {hlwhich, 1, false},
}
var hshMod *rt.Table
@ -62,9 +64,7 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
if hshMod == nil {
hshMod = mod
}
hshMod = mod
host, _ := os.Hostname()
username := curuser.Username
@ -104,6 +104,7 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
// hilbish.completion table
hshcomp := completionLoader(rtm)
// TODO: REMOVE "completion" AND ONLY USE "completions" WITH AN S
mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp))
mod.Set(rt.StringValue("completions"), rt.TableValue(hshcomp))
// hilbish.runner table
@ -120,6 +121,9 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
timersModule := timers.loader(rtm)
mod.Set(rt.StringValue("timers"), rt.TableValue(timersModule))
editorModule := editorLoader(rtm)
mod.Set(rt.StringValue("editor"), rt.TableValue(editorModule))
versionModule := rt.NewTable()
util.SetField(rtm, versionModule, "branch", rt.StringValue(gitBranch))
util.SetField(rtm, versionModule, "full", rt.StringValue(getVersion()))
@ -130,18 +134,15 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) {
pluginModule := moduleLoader(rtm)
mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule))
sinkModule := util.SinkLoader(rtm)
mod.Set(rt.StringValue("sink"), rt.TableValue(sinkModule))
return rt.TableValue(mod), nil
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if len(value) == 0 {
return fallback
}
return value
value := os.Getenv(key)
if len(value) == 0 {
return fallback
}
return value
}
func setVimMode(mode string) {
@ -153,7 +154,6 @@ func unsetVimMode() {
util.SetField(l, hshMod, "vimMode", rt.NilValue)
}
/*
func handleStream(v rt.Value, strms *streams, errStream bool) error {
ud, ok := v.TryUserData()
if !ok {
@ -182,7 +182,112 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error {
return nil
}
// run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
// Runs `cmd` in Hilbish's shell script interpreter.
// The `streams` parameter specifies the output and input streams the command should use.
// For example, to write command output to a sink.
// As a table, the caller can directly specify the standard output, error, and input
// streams of the command with the table keys `out`, `err`, and `input` respectively.
// As a boolean, it specifies whether the command should use standard output or return its output streams.
// #param cmd string
// #param streams table|boolean
// #returns number, string, string
// #example
/*
// This code is the same as `ls -l | wc -l`
local fs = require 'fs'
local pr, pw = fs.pipe()
hilbish.run('ls -l', {
stdout = pw,
stderr = pw,
})
pw:close()
hilbish.run('wc -l', {
stdin = pr
})
*/
// #example
func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN.
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
strms := &streams{}
var terminalOut bool
if len(c.Etc()) != 0 {
tout := c.Etc()[0]
var ok bool
terminalOut, ok = tout.TryBool()
if !ok {
luastreams, ok := tout.TryTable()
if !ok {
return nil, errors.New("bad argument to run (expected boolean or table, got " + tout.TypeName() + ")")
}
handleStream(luastreams.Get(rt.StringValue("out")), strms, false)
handleStream(luastreams.Get(rt.StringValue("err")), strms, true)
stdinstrm := luastreams.Get(rt.StringValue("input"))
if !stdinstrm.IsNil() {
ud, ok := stdinstrm.TryUserData()
if !ok {
return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file, got " + stdinstrm.TypeName() + ")")
}
val := ud.Value()
var varstrm io.Reader
if f, ok := val.(*iolib.File); ok {
varstrm = f.Handle()
}
if f, ok := val.(*sink); ok {
varstrm = f.reader
}
if varstrm == nil {
return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file)")
}
strms.stdin = varstrm
}
} else {
if !terminalOut {
strms = &streams{
stdout: new(bytes.Buffer),
stderr: new(bytes.Buffer),
}
}
}
}
var exitcode uint8
stdout, stderr, err := execCommand(cmd, strms)
if code, ok := interp.IsExitStatus(err); ok {
exitcode = code
} else if err != nil {
exitcode = 1
}
var stdoutStr, stderrStr string
if stdoutBuf, ok := stdout.(*bytes.Buffer); ok {
stdoutStr = stdoutBuf.String()
}
if stderrBuf, ok := stderr.(*bytes.Buffer); ok {
stderrStr = stderrBuf.String()
}
return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil
}
// cwd() -> string
// Returns the current directory of the shell.
@ -193,6 +298,7 @@ func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil
}
// read(prompt) -> input (string)
// Read input from the user, using Hilbish's line editor/input reader.
// This is a separate instance from the one Hilbish actually uses.
@ -263,13 +369,11 @@ func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
}
switch typ {
case "left":
prompt = p
lr.SetPrompt(fmtPrompt(prompt))
case "right":
lr.SetRightPrompt(fmtPrompt(p))
default:
return nil, errors.New("expected prompt type to be right or left, got " + typ)
case "left":
prompt = p
lr.SetPrompt(fmtPrompt(prompt))
case "right": lr.SetRightPrompt(fmtPrompt(p))
default: return nil, errors.New("expected prompt type to be right or left, got " + typ)
}
return c.Next(), nil
@ -300,7 +404,7 @@ hilbish.multiprompt '-->'
*/
func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return c.PushingNext1(t.Runtime, rt.StringValue(multilinePrompt)), nil
return nil, err
}
prompt, err := c.StringArg(0)
if err != nil {
@ -386,7 +490,7 @@ func appendPath(dir string) {
// if dir isnt already in $PATH, add it
if !strings.Contains(pathenv, dir) {
os.Setenv("PATH", pathenv+string(os.PathListSeparator)+dir)
os.Setenv("PATH", pathenv + string(os.PathListSeparator) + dir)
}
}
@ -404,7 +508,7 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
}
cmdArgs, _ := splitInput(cmd)
if runtime.GOOS != "windows" {
cmdPath, err := util.LookPath(cmdArgs[0])
cmdPath, err := exec.LookPath(cmdArgs[0])
if err != nil {
fmt.Println(err)
// if we get here, cmdPath will be nothing
@ -571,7 +675,7 @@ func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// if dir isnt already in $PATH, add in
if !strings.Contains(pathenv, dir) {
os.Setenv("PATH", dir+string(os.PathListSeparator)+pathenv)
os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv)
}
return c.Next(), nil
@ -602,7 +706,7 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil
}
path, err := util.LookPath(cmd)
path, err := exec.LookPath(cmd)
if err != nil {
return c.Next(), nil
}
@ -625,14 +729,42 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
}
switch mode {
case "emacs":
unsetVimMode()
lr.rl.InputMode = readline.Emacs
case "vim":
setVimMode("insert")
lr.rl.InputMode = readline.Vim
default:
return nil, errors.New("inputMode: expected vim or emacs, received " + mode)
case "emacs":
unsetVimMode()
lr.rl.InputMode = readline.Emacs
case "vim":
setVimMode("insert")
lr.rl.InputMode = readline.Vim
default:
return nil, errors.New("inputMode: expected vim or emacs, received " + mode)
}
return c.Next(), nil
}
// 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.
// Read [about runner mode](../features/runner-mode) for more information.
// #param mode string|function
func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
mode := c.Arg(0)
switch mode.Type() {
case rt.StringType:
switch mode.AsString() {
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
@ -667,9 +799,7 @@ func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// #example
// --This code will highlight all double quoted strings in green.
// function hilbish.highlighter(line)
//
// return line:gsub('"%w+"', function(c) return lunacolors.green(c) end)
//
// return line:gsub('"%w+"', function(c) return lunacolors.green(c) end)
// end
// #example
// #param line string

View File

@ -2,14 +2,14 @@ package main
import (
"fmt"
"path/filepath"
"go/ast"
"go/doc"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
"os"
"sync"
md "github.com/atsushinee/go-markdown-generator/doc"
@ -27,49 +27,49 @@ menu:
`
type emmyPiece struct {
DocPiece *docPiece
DocPiece *docPiece
Annotations []string
Params []string // we only need to know param name to put in function
FuncName string
Params []string // we only need to know param name to put in function
FuncName string
}
type module struct {
Types []docPiece
Docs []docPiece
Fields []docPiece
Properties []docPiece
Types []docPiece
Docs []docPiece
Fields []docPiece
Properties []docPiece
ShortDescription string
Description string
ParentModule string
HasInterfaces bool
HasTypes bool
Description string
ParentModule string
HasInterfaces bool
HasTypes bool
}
type param struct {
type param struct{
Name string
Type string
Doc []string
Doc []string
}
type docPiece struct {
Doc []string
FuncSig string
FuncName string
Interfacing string
Doc []string
FuncSig string
FuncName string
Interfacing string
ParentModule string
GoFuncName string
IsInterface bool
IsMember bool
IsType bool
Fields []docPiece
Properties []docPiece
Params []param
Tags map[string][]tag
GoFuncName string
IsInterface bool
IsMember bool
IsType bool
Fields []docPiece
Properties []docPiece
Params []param
Tags map[string][]tag
}
type tag struct {
id string
fields []string
id string
fields []string
startIdx int
}
@ -78,15 +78,12 @@ var interfaceDocs = make(map[string]module)
var emmyDocs = make(map[string][]emmyPiece)
var typeTable = make(map[string][]string) // [0] = parentMod, [1] = interfaces
var prefix = map[string]string{
"main": "hl",
"hilbish": "hl",
"fs": "f",
"main": "hl",
"hilbish": "hl",
"fs": "f",
"commander": "c",
"bait": "b",
"terminal": "term",
"snail": "snail",
"readline": "rl",
"yarn": "yarn",
"bait": "b",
"terminal": "term",
}
func getTagsAndDocs(docs string) (map[string][]tag, []string) {
@ -111,7 +108,7 @@ func getTagsAndDocs(docs string) (map[string][]tag, []string) {
} else {
if tagParts[0] == "example" {
exampleIdx := tags["example"][0].startIdx
exampleCode := pts[exampleIdx+1 : idx]
exampleCode := pts[exampleIdx+1:idx]
tags["example"][0].fields = exampleCode
parts = strings.Split(strings.Replace(strings.Join(parts, "\n"), strings.TrimPrefix(strings.Join(exampleCode, "\n"), "#example\n"), "", -1), "\n")
@ -123,7 +120,7 @@ func getTagsAndDocs(docs string) (map[string][]tag, []string) {
fleds = tagParts[2:]
}
tags[tagParts[0]] = append(tags[tagParts[0]], tag{
id: tagParts[1],
id: tagParts[1],
fields: fleds,
})
}
@ -140,7 +137,7 @@ func docPieceTag(tagName string, tags map[string][]tag) []docPiece {
for _, tag := range tags[tagName] {
dps = append(dps, docPiece{
FuncName: tag.id,
Doc: tag.fields,
Doc: tag.fields,
})
}
@ -171,16 +168,16 @@ func setupDocType(mod string, typ *doc.Type) *docPiece {
if strings.HasPrefix(d, "---") {
// TODO: document types in lua
/*
emmyLine := strings.TrimSpace(strings.TrimPrefix(d, "---"))
emmyLinePieces := strings.Split(emmyLine, " ")
emmyType := emmyLinePieces[0]
if emmyType == "@param" {
em.Params = append(em.Params, emmyLinePieces[1])
}
if emmyType == "@vararg" {
em.Params = append(em.Params, "...") // add vararg
}
em.Annotations = append(em.Annotations, d)
emmyLine := strings.TrimSpace(strings.TrimPrefix(d, "---"))
emmyLinePieces := strings.Split(emmyLine, " ")
emmyType := emmyLinePieces[0]
if emmyType == "@param" {
em.Params = append(em.Params, emmyLinePieces[1])
}
if emmyType == "@vararg" {
em.Params = append(em.Params, "...") // add vararg
}
em.Annotations = append(em.Annotations, d)
*/
} else {
typeDoc = append(typeDoc, d)
@ -193,16 +190,16 @@ func setupDocType(mod string, typ *doc.Type) *docPiece {
}
parentMod := mod
dps := &docPiece{
Doc: typeDoc,
FuncName: typeName,
Interfacing: interfaces,
IsInterface: inInterface,
IsMember: isMember,
IsType: true,
Doc: typeDoc,
FuncName: typeName,
Interfacing: interfaces,
IsInterface: inInterface,
IsMember: isMember,
IsType: true,
ParentModule: parentMod,
Fields: fields,
Properties: properties,
Tags: tags,
Fields: fields,
Properties: properties,
Tags: tags,
}
typeTable[strings.ToLower(typeName)] = []string{parentMod, interfaces}
@ -211,10 +208,6 @@ func setupDocType(mod string, typ *doc.Type) *docPiece {
}
func setupDoc(mod string, fun *doc.Func) *docPiece {
if fun.Doc == "" {
return nil
}
docs := strings.TrimSpace(fun.Doc)
tags, parts := getTagsAndDocs(docs)
@ -223,10 +216,6 @@ func setupDoc(mod string, fun *doc.Func) *docPiece {
goto start
}
if prefix[mod] == "" {
return nil
}
if (!strings.HasPrefix(fun.Name, prefix[mod]) && tags["interface"] == nil) || (strings.ToLower(fun.Name) == "loader" && tags["interface"] == nil) {
return nil
}
@ -254,7 +243,7 @@ start:
params[i] = param{
Name: p.id,
Type: p.fields[0],
Doc: p.fields[1:],
Doc: p.fields[1:],
}
}
}
@ -285,18 +274,18 @@ start:
parentMod = mod
}
dps := &docPiece{
Doc: funcdoc,
FuncSig: funcsig,
FuncName: funcName,
Interfacing: interfaces,
GoFuncName: strings.ToLower(fun.Name),
IsInterface: inInterface,
IsMember: isMember,
Doc: funcdoc,
FuncSig: funcsig,
FuncName: funcName,
Interfacing: interfaces,
GoFuncName: strings.ToLower(fun.Name),
IsInterface: inInterface,
IsMember: isMember,
ParentModule: parentMod,
Fields: fields,
Properties: properties,
Params: params,
Tags: tags,
Fields: fields,
Properties: properties,
Params: params,
Tags: tags,
}
if strings.HasSuffix(dps.GoFuncName, strings.ToLower("loader")) {
dps.Doc = parts
@ -310,33 +299,15 @@ start:
func main() {
fset := token.NewFileSet()
os.Mkdir("docs", 0777)
os.RemoveAll("docs/api")
os.Mkdir("docs/api", 0777)
f, err := os.Create("docs/api/_index.md")
if err != nil {
panic(err)
}
f.WriteString(`---
title: API
layout: doc
weight: -100
menu: docs
---
Welcome to the API documentation for Hilbish. This documents Lua functions
provided by Hilbish.
`)
f.Close()
os.Mkdir("emmyLuaDocs", 0777)
dirs := []string{"./", "./util"}
filepath.Walk("golibs/", func(path string, info os.FileInfo, err error) error {
dirs := []string{"./"}
filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error {
if !info.IsDir() {
return nil
}
dirs = append(dirs, "./"+path)
dirs = append(dirs, "./" + path)
return nil
})
@ -358,7 +329,7 @@ provided by Hilbish.
pieces := []docPiece{}
typePieces := []docPiece{}
mod := l
if mod == "main" || mod == "util" {
if mod == "main" {
mod = "hilbish"
}
var hasInterfaces bool
@ -442,23 +413,14 @@ provided by Hilbish.
interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece)
}
fmt.Println(filteredTypePieces)
if newDoc, ok := docs[mod]; ok {
oldMod := docs[mod]
newDoc.Types = append(filteredTypePieces, oldMod.Types...)
newDoc.Docs = append(filteredPieces, oldMod.Docs...)
docs[mod] = newDoc
} else {
docs[mod] = module{
Types: filteredTypePieces,
Docs: filteredPieces,
ShortDescription: shortDesc,
Description: strings.Join(desc, "\n"),
HasInterfaces: hasInterfaces,
Properties: docPieceTag("property", tags),
Fields: docPieceTag("field", tags),
}
docs[mod] = module{
Types: filteredTypePieces,
Docs: filteredPieces,
ShortDescription: shortDesc,
Description: strings.Join(desc, "\n"),
HasInterfaces: hasInterfaces,
Properties: docPieceTag("property", tags),
Fields: docPieceTag("field", tags),
}
}
@ -472,7 +434,7 @@ provided by Hilbish.
for mod, v := range docs {
docPath := "docs/api/" + mod + ".md"
if v.HasInterfaces {
os.Mkdir("docs/api/"+mod, 0777)
os.Mkdir("docs/api/" + mod, 0777)
os.Remove(docPath) // remove old doc path if it exists
docPath = "docs/api/" + mod + "/_index.md"
}
@ -525,11 +487,11 @@ provided by Hilbish.
continue
}
mdTable.SetContent(i-diff, 0, fmt.Sprintf(`<a href="#%s">%s</a>`, dps.FuncName, dps.FuncSig))
mdTable.SetContent(i - diff, 0, fmt.Sprintf(`<a href="#%s">%s</a>`, dps.FuncName, dps.FuncSig))
if len(dps.Doc) == 0 {
fmt.Printf("WARNING! Function %s on module %s has no documentation!\n", dps.FuncName, modname)
} else {
mdTable.SetContent(i-diff, 1, dps.Doc[0])
mdTable.SetContent(i - diff, 1, dps.Doc[0])
}
}
f.WriteString(mdTable.String())
@ -543,6 +505,7 @@ provided by Hilbish.
mdTable.SetTitle(0, "")
mdTable.SetTitle(1, "")
for i, dps := range modu.Fields {
mdTable.SetContent(i, 0, dps.FuncName)
mdTable.SetContent(i, 1, strings.Join(dps.Doc, " "))
@ -557,6 +520,7 @@ provided by Hilbish.
mdTable.SetTitle(0, "")
mdTable.SetTitle(1, "")
for i, dps := range modu.Properties {
mdTable.SetContent(i, 0, dps.FuncName)
mdTable.SetContent(i, 1, strings.Join(dps.Doc, " "))
@ -574,7 +538,7 @@ provided by Hilbish.
continue
}
f.WriteString(fmt.Sprintf("<hr>\n<div id='%s'>", dps.FuncName))
htmlSig := typeTag.ReplaceAllStringFunc(strings.Replace(modname+"."+dps.FuncSig, "<", `\<`, -1), func(typ string) string {
htmlSig := typeTag.ReplaceAllStringFunc(strings.Replace(modname + "." + dps.FuncSig, "<", `\<`, -1), func(typ string) string {
typName := typ[1:]
typLookup := typeTable[strings.ToLower(typName)]
ifaces := typLookup[0] + "." + typLookup[1] + "/"
@ -661,7 +625,7 @@ provided by Hilbish.
typName := regexp.MustCompile(`\w+`).FindString(typ[1:])
typLookup := typeTable[strings.ToLower(typName)]
fmt.Printf("%+q, \n", typLookup)
linkedTyp := fmt.Sprintf("/Hilbish/docs/api/%s/%s/#%s", typLookup[0], typLookup[0]+"."+typLookup[1], strings.ToLower(typName))
linkedTyp := fmt.Sprintf("/Hilbish/docs/api/%s/%s/#%s", typLookup[0], typLookup[0] + "." + typLookup[1], strings.ToLower(typName))
return fmt.Sprintf(`<a href="#%s" style="text-decoration: none;">%s</a>`, linkedTyp, typName)
})
f.WriteString(fmt.Sprintf("#### %s\n", htmlSig))

View File

@ -1,9 +1,7 @@
local fs = require 'fs'
local emmyPattern = '^%-%-%- (.+)'
local emmyPattern2 = '^%-%- (.+)'
local modpattern = '^%-+ @module (.+)'
local modpattern = '^%-+ @module (%w+)'
local pieces = {}
local descriptions = {}
local files = fs.readdir 'nature'
for _, fname in ipairs(files) do
@ -15,25 +13,18 @@ for _, fname in ipairs(files) do
local mod = header:match(modpattern)
if not mod then goto continue end
print(fname, mod)
pieces[mod] = {}
descriptions[mod] = {}
local docPiece = {}
local lines = {}
local lineno = 0
local doingDescription = true
for line in f:lines() do
lineno = lineno + 1
lines[lineno] = line
if line == header then goto continue2 end
if line:match(emmyPattern) or line:match(emmyPattern2) then
if doingDescription then
table.insert(descriptions[mod], line:match(emmyPattern) or line:match(emmyPattern2))
end
else
doingDescription = false
if not line:match(emmyPattern) then
if line:match '^function' then
local pattern = (string.format('^function %s%%.', mod) .. '(%w+)')
local funcName = line:match(pattern)
@ -41,12 +32,10 @@ for _, fname in ipairs(files) do
local dps = {
description = {},
example = {},
params = {}
}
local offset = 1
local doingExample = false
while true do
local prev = lines[lineno - offset]
@ -62,23 +51,11 @@ for _, fname in ipairs(files) do
if emmy == 'param' then
table.insert(dps.params, 1, {
name = emmythings[1],
type = emmythings[2],
-- the +1 accounts for space.
description = table.concat(emmythings, ' '):sub(emmythings[1]:len() + 1 + emmythings[2]:len() + 1)
type = emmythings[2]
})
end
else
if docline:match '#example' then
doingExample = not doingExample
end
if not docline:match '#example' then
if doingExample then
table.insert(dps.example, 1, docline)
else
table.insert(dps.description, 1, docline)
end
end
table.insert(dps.description, 1, docline)
end
offset = offset + 1
else
@ -86,7 +63,7 @@ for _, fname in ipairs(files) do
end
end
table.insert(pieces[mod], {funcName, dps})
pieces[mod][funcName] = dps
end
docPiece = {}
goto continue2
@ -104,82 +81,29 @@ description: %s
layout: doc
menu:
docs:
parent: "%s"
parent: "Nature"
---
]]
for iface, dps in pairs(pieces) do
local mod = iface:match '(%w+)%.' or 'nature'
local docParent = 'Nature'
path = string.format('docs/%s/%s.md', mod, iface)
if mod ~= 'nature' then
docParent = "API"
path = string.format('docs/api/%s/%s.md', mod, iface)
end
if iface == 'hilbish' then
docParent = "API"
path = string.format('docs/api/hilbish/_index.md', mod, iface)
end
local path = string.format('docs/%s/%s.md', mod, iface)
fs.mkdir(fs.dir(path), true)
local f <close> = io.open(path, 'w')
f:write(string.format(header, 'Module', iface, 'No description.'))
print(f)
local exists = pcall(fs.stat, path)
local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish'
print(mod, path)
local f <close> = io.open(path, newOrNotNature and 'r+' or 'w+')
local tocPos
if not newOrNotNature then
f:write(string.format(header, 'Module', iface, (descriptions[iface] and #descriptions[iface] > 0) and descriptions[iface][1] or 'No description.', docParent))
if descriptions[iface] and #descriptions[iface] > 0 then
table.remove(descriptions[iface], 1)
f:write(string.format('\n## Introduction\n%s\n\n', table.concat(descriptions[iface], '\n')))
f:write('## Functions\n')
f:write([[|||
|----|----|
]])
tocPos = f:seek()
end
end
local tocSearch = false
for line in f:lines() do
if line:match '^## Functions' then
tocSearch = true
end
if tocSearch and line == '' then
tocSearch = false
tocPos = f:seek() - 1
end
end
table.sort(dps, function(a, b) return a[1] < b[1] end)
for _, piece in pairs(dps) do
local func = piece[1]
local docs = piece[2]
for func, docs in pairs(dps) do
f:write(string.format('<hr>\n<div id=\'%s\'>', func))
local sig = string.format('%s.%s(', iface, func)
local params = ''
for idx, param in ipairs(docs.params) do
sig = sig .. param.name:gsub('%?$', '')
params = params .. param.name:gsub('%?$', '')
if idx ~= #docs.params then
sig = sig .. ', '
params = params .. ', '
end
sig = sig .. ((param.name:gsub('%?$', '')))
if idx ~= #docs.params then sig = sig .. ', ' end
end
sig = sig .. ')'
if tocPos then
f:seek('set', tocPos)
local contents = f:read '*a'
f:seek('set', tocPos)
local tocLine = string.format('|<a href="#%s">%s</a>|%s|\n', func, string.format('%s(%s)', func, params), docs.description[1])
f:write(tocLine .. contents)
f:seek 'end'
end
f:write(string.format('<hr>\n<div id=\'%s\'>\n', func))
f:write(string.format([[
<h4 class='heading'>
%s
@ -196,12 +120,7 @@ for iface, dps in pairs(pieces) do
f:write 'This function has no parameters. \n'
end
for _, param in ipairs(docs.params) do
f:write(string.format('`%s` **`%s`** \n', param.name:gsub('%?$', ''), param.type))
f:write(string.format('%s\n\n', param.description))
end
if #docs.example ~= 0 then
f:write '#### Example\n'
f:write(string.format('```lua\n%s\n```\n', table.concat(docs.example, '\n')))
f:write(string.format('`%s` **`%s`**\n', param.name:gsub('%?$', ''), param.type))
end
--[[
local params = table.filter(docs, function(t)

View File

@ -98,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
if len(fileCompletions) != 0 {
for _, f := range fileCompletions {
fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
continue
}
completions = append(completions, f)
@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
// get basename from matches
for _, match := range matches {
// check if we have execute permissions for our match
err := util.FindExecutable(match, true, false)
err := findExecutable(match, true, false)
if err != nil {
continue
}
@ -157,12 +157,9 @@ func matchPath(query string) ([]string, string) {
files, _ := os.ReadDir(path)
for _, entry := range files {
// should we handle errors here?
file, err := entry.Info()
if err != nil {
continue
}
if file.Mode() & os.ModeSymlink != 0 {
if err == nil && file.Mode() & os.ModeSymlink != 0 {
path, err := filepath.EvalSymlinks(filepath.Join(path, file.Name()))
if err == nil {
file, err = os.Lstat(path)

View File

@ -28,10 +28,10 @@ interfaces and functions which directly relate to shell functionality.
|<a href="#prependPath">prependPath(dir)</a>|Prepends `dir` to $PATH.|
|<a href="#prompt">prompt(str, typ)</a>|Changes the shell prompt to the provided string.|
|<a href="#read">read(prompt) -> input (string)</a>|Read input from the user, using Hilbish's line editor/input reader.|
|<a href="#run">run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)</a>|Runs `cmd` in Hilbish's shell script interpreter.|
|<a href="#runnerMode">runnerMode(mode)</a>|Sets the execution/runner mode for interactive Hilbish.|
|<a href="#timeout">timeout(cb, time) -> @Timer</a>|Executed the `cb` function after a period of `time`.|
|<a href="#which">which(name) -> string</a>|Checks if `name` is a valid command.|
|<a href="#runnerMode">runnerMode(mode)</a>|Sets the execution/runner mode for interactive Hilbish.|
|<a href="#run">run(cmd, streams)</a>|Runs `cmd` in Hilbish's shell script interpreter.|
## Static module fields
|||
@ -229,9 +229,7 @@ Note that to set a highlighter, one has to override this function.
```lua
--This code will highlight all double quoted strings in green.
function hilbish.highlighter(line)
return line:gsub('"%w+"', function(c) return lunacolors.green(c) end)
return line:gsub('"%w+"', function(c) return lunacolors.green(c) end)
end
```
</div>
@ -410,6 +408,72 @@ Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
`string` **`prompt?`**
Text to print before input, can be empty.
</div>
<hr>
<div id='run'>
<h4 class='heading'>
hilbish.run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)
<a href="#run" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Runs `cmd` in Hilbish's shell script interpreter.
The `streams` parameter specifies the output and input streams the command should use.
For example, to write command output to a sink.
As a table, the caller can directly specify the standard output, error, and input
streams of the command with the table keys `out`, `err`, and `input` respectively.
As a boolean, it specifies whether the command should use standard output or return its output streams.
#### Parameters
`string` **`cmd`**
`table|boolean` **`streams`**
#### Example
```lua
// This code is the same as `ls -l | wc -l`
local fs = require 'fs'
local pr, pw = fs.pipe()
hilbish.run('ls -l', {
stdout = pw,
stderr = pw,
})
pw:close()
hilbish.run('wc -l', {
stdin = pr
})
```
</div>
<hr>
<div id='runnerMode'>
<h4 class='heading'>
hilbish.runnerMode(mode)
<a href="#runnerMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
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.
Read [about runner mode](../features/runner-mode) for more information.
#### Parameters
`string|function` **`mode`**
</div>
<hr>
@ -455,7 +519,8 @@ Will return the path of the binary, or a basename if it's a commander.
<hr>
## Sink
A sink is a structure that has input and/or output to/from a desination.
A sink is a structure that has input and/or output to/from
a desination.
### Methods
#### autoFlush(auto)
@ -477,65 +542,3 @@ Writes data to a sink.
#### writeln(str)
Writes data to a sink with a newline at the end.
<hr>
<div id='run'>
<h4 class='heading'>
hilbish.run(cmd, streams)
<a href="#run" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Runs `cmd` in Hilbish's shell script interpreter.
The `streams` parameter specifies the output and input streams the command should use.
For example, to write command output to a sink.
As a table, the caller can directly specify the standard output, error, and input
streams of the command with the table keys `out`, `err`, and `input` respectively.
As a boolean, it specifies whether the command should use standard output or return its output streams.
#### Parameters
`cmd` **`string`**
`streams` **`table|boolean`**
#### Example
```lua
-- This code is the same as `ls -l | wc -l`
local fs = require 'fs'
local pr, pw = fs.pipe()
hilbish.run('ls -l', {
stdout = pw,
stderr = pw,
})
pw:close()
hilbish.run('wc -l', {
stdin = pr
})
```
</div>
<hr>
<div id='runnerMode'>
<h4 class='heading'>
hilbish.runnerMode(mode)
<a href="#runnerMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sets the execution/runner mode for interactive Hilbish.
**NOTE: This function is deprecated and will be removed in 3.0**
Use `hilbish.runner.setCurrent` instead.
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.
Read [about runner mode](../features/runner-mode) for more information.
#### Parameters
`mode` **`string|function`**
</div>

View File

@ -1,67 +0,0 @@
---
title: Module hilbish.abbr
description: command line abbreviations
layout: doc
menu:
docs:
parent: "API"
---
## Introduction
The abbr module manages Hilbish abbreviations. These are words that can be replaced
with longer command line strings when entered.
As an example, `git push` can be abbreviated to `gp`. When the user types
`gp` into the command line, after hitting space or enter, it will expand to `git push`.
Abbreviations can be used as an alternative to aliases. They are saved entirely in the history
Instead of the aliased form of the same command.
## Functions
|||
|----|----|
|<a href="#remove">remove(abbr)</a>|Removes the named `abbr`.|
|<a href="#add">add(abbr, expanded|function, opts)</a>|Adds an abbreviation. The `abbr` is the abbreviation itself,|
<hr>
<div id='add'>
<h4 class='heading'>
hilbish.abbr.add(abbr, expanded|function, opts)
<a href="#add" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Adds an abbreviation. The `abbr` is the abbreviation itself,
while `expanded` is what the abbreviation should expand to.
It can be either a function or a string. If it is a function, it will expand to what
the function returns.
`opts` is a table that accepts 1 key: `anywhere`.
`opts.anywhere` defines whether the abbr expands anywhere in the command line or not,
whereas the default behavior is only at the beginning of the line
#### Parameters
`abbr` **`string`**
`expanded|function` **`string`**
`opts` **`table`**
</div>
<hr>
<div id='remove'>
<h4 class='heading'>
hilbish.abbr.remove(abbr)
<a href="#remove" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Removes the named `abbr`.
#### Parameters
`abbr` **`string`**
</div>

View File

@ -0,0 +1,103 @@
---
title: Module hilbish.editor
description: interactions for Hilbish's line reader
layout: doc
menu:
docs:
parent: "API"
---
## Introduction
The hilbish.editor interface provides functions to
directly interact with the line editor in use.
## Functions
|||
|----|----|
|<a href="#editor.getLine">getLine() -> string</a>|Returns the current input line.|
|<a href="#editor.getVimRegister">getVimRegister(register) -> string</a>|Returns the text that is at the register.|
|<a href="#editor.insert">insert(text)</a>|Inserts text into the Hilbish command line.|
|<a href="#editor.getChar">getChar() -> string</a>|Reads a keystroke from the user. This is in a format of something like Ctrl-L.|
|<a href="#editor.setVimRegister">setVimRegister(register, text)</a>|Sets the vim register at `register` to hold the passed text.|
<hr>
<div id='editor.getLine'>
<h4 class='heading'>
hilbish.editor.getLine() -> string
<a href="#editor.getLine" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns the current input line.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='editor.getVimRegister'>
<h4 class='heading'>
hilbish.editor.getVimRegister(register) -> string
<a href="#editor.getVimRegister" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns the text that is at the register.
#### Parameters
`string` **`register`**
</div>
<hr>
<div id='editor.insert'>
<h4 class='heading'>
hilbish.editor.insert(text)
<a href="#editor.insert" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Inserts text into the Hilbish command line.
#### Parameters
`string` **`text`**
</div>
<hr>
<div id='editor.getChar'>
<h4 class='heading'>
hilbish.editor.getChar() -> string
<a href="#editor.getChar" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Reads a keystroke from the user. This is in a format of something like Ctrl-L.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='editor.setVimRegister'>
<h4 class='heading'>
hilbish.editor.setVimRegister(register, text)
<a href="#editor.setVimRegister" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sets the vim register at `register` to hold the passed text.
#### Parameters
`string` **`text`**
</div>

View File

@ -1,135 +0,0 @@
---
title: Module hilbish.messages
description: simplistic message passing
layout: doc
menu:
docs:
parent: "API"
---
## Introduction
The messages interface defines a way for Hilbish-integrated commands,
user config and other tasks to send notifications to alert the user.z
The `hilbish.message` type is a table with the following keys:
`title` (string): A title for the message notification.
`text` (string): The contents of the message.
`channel` (string): States the origin of the message, `hilbish.*` is reserved for Hilbish tasks.
`summary` (string): A short summary of the `text`.
`icon` (string): Unicode (preferably standard emoji) icon for the message notification
`read` (boolean): Whether the full message has been read or not.
## Functions
|||
|----|----|
|<a href="#unreadCount">unreadCount()</a>|Returns the amount of unread messages.|
|<a href="#send">send(message)</a>|Sends a message.|
|<a href="#readAll">readAll()</a>|Marks all messages as read.|
|<a href="#read">read(idx)</a>|Marks a message at `idx` as read.|
|<a href="#delete">delete(idx)</a>|Deletes the message at `idx`.|
|<a href="#clear">clear()</a>|Deletes all messages.|
|<a href="#all">all()</a>|Returns all messages.|
<hr>
<div id='all'>
<h4 class='heading'>
hilbish.messages.all()
<a href="#all" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns all messages.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='clear'>
<h4 class='heading'>
hilbish.messages.clear()
<a href="#clear" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Deletes all messages.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='delete'>
<h4 class='heading'>
hilbish.messages.delete(idx)
<a href="#delete" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Deletes the message at `idx`.
#### Parameters
`idx` **`number`**
</div>
<hr>
<div id='read'>
<h4 class='heading'>
hilbish.messages.read(idx)
<a href="#read" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Marks a message at `idx` as read.
#### Parameters
`idx` **`number`**
</div>
<hr>
<div id='readAll'>
<h4 class='heading'>
hilbish.messages.readAll()
<a href="#readAll" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Marks all messages as read.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='send'>
<h4 class='heading'>
hilbish.messages.send(message)
<a href="#send" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sends a message.
#### Parameters
`message` **`hilbish.message`**
</div>
<hr>
<div id='unreadCount'>
<h4 class='heading'>
hilbish.messages.unreadCount()
<a href="#unreadCount" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns the amount of unread messages.
#### Parameters
This function has no parameters.
</div>

View File

@ -1,39 +0,0 @@
---
title: Module hilbish.processors
description: No description.
layout: doc
menu:
docs:
parent: "API"
---
<hr>
<div id='add'>
<h4 class='heading'>
hilbish.processors.add()
<a href="#add" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='execute'>
<h4 class='heading'>
hilbish.processors.execute()
<a href="#execute" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Run all command processors, in order by priority.
It returns the processed command (which may be the same as the passed command)
and a boolean which states whether to proceed with command execution.
#### Parameters
This function has no parameters.
</div>

View File

@ -21,18 +21,16 @@ A runner is passed the input and has to return a table with these values.
All are not required, only the useful ones the runner needs to return.
(So if there isn't an error, just omit `err`.)
- `exitCode` (number): Exit code of the command
- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case
more is requested.
- `err` (string): A string that represents an error from the runner.
This should only be set when, for example, there is a syntax error.
It can be set to a few special values for Hilbish to throw the right
hooks and have a better looking message.
- `\<command>: not-found` will throw a `command.not-found` hook
based on what `\<command>` is.
- `\<command>: not-executable` will throw a `command.not-executable` hook.
- `continue` (boolean): Whether Hilbish should prompt the user for no input
- `newline` (boolean): Whether a newline should be added at the end of `input`.
- `exitCode` (number): A numerical code to indicate the exit result.
- `input` (string): The user input. This will be used to add
to the history.
- `err` (string): A string to indicate an interal error for the runner.
It can be set to a few special values for Hilbish to throw the right hooks and have a better looking message:
`[command]: not-found` will throw a command.not-found hook based on what `[command]` is.
`[command]: not-executable` will throw a command.not-executable hook.
- `continue` (boolean): Whether to prompt the user for more input.
Here is a simple example of a fennel runner. It falls back to
shell script if fennel eval has an error.
@ -54,16 +52,29 @@ end)
## Functions
|||
|----|----|
|<a href="#runner.setMode">setMode(cb)</a>|This is the same as the `hilbish.runnerMode` function.|
|<a href="#runner.lua">lua(cmd)</a>|Evaluates `cmd` as Lua input. This is the same as using `dofile`|
|<a href="#sh">sh()</a>|nil|
|<a href="#setMode">setMode(mode)</a>|**NOTE: This function is deprecated and will be removed in 3.0**|
|<a href="#setCurrent">setCurrent(name)</a>|Sets Hilbish's runner mode by name.|
|<a href="#set">set(name, runner)</a>|*Sets* a runner by name. The difference between this function and|
|<a href="#run">run(input, priv)</a>|Runs `input` with the currently set Hilbish runner.|
|<a href="#getCurrent">getCurrent()</a>|Returns the current runner by name.|
|<a href="#get">get(name)</a>|Get a runner by name.|
|<a href="#exec">exec(cmd, runnerName)</a>|Executes `cmd` with a runner.|
|<a href="#add">add(name, runner)</a>|Adds a runner to the table of available runners.|
|<a href="#runner.sh">sh(cmd)</a>|Runs a command in Hilbish's shell script interpreter.|
<hr>
<div id='runner.setMode'>
<h4 class='heading'>
hilbish.runner.setMode(cb)
<a href="#runner.setMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
This is the same as the `hilbish.runnerMode` function.
It takes a callback, which will be used to execute all interactive input.
In normal cases, neither callbacks should be overrided by the user,
as the higher level functions listed below this will handle it.
#### Parameters
`function` **`cb`**
</div>
<hr>
<div id='runner.lua'>
@ -84,164 +95,20 @@ or `load`, but is appropriated for the runner interface.
</div>
<hr>
<div id='add'>
<div id='runner.sh'>
<h4 class='heading'>
hilbish.runner.add(name, runner)
<a href="#add" class='heading-link'>
hilbish.runner.sh(cmd)
<a href="#runner.sh" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Adds a runner to the table of available runners.
If runner is a table, it must have the run function in it.
#### Parameters
`name` **`string`**
Name of the runner
Runs a command in Hilbish's shell script interpreter.
This is the equivalent of using `source`.
`runner` **`function|table`**
#### Parameters
`string` **`cmd`**
</div>
<hr>
<div id='exec'>
<h4 class='heading'>
hilbish.runner.exec(cmd, runnerName)
<a href="#exec" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Executes `cmd` with a runner.
If `runnerName` is not specified, it uses the default Hilbish runner.
#### Parameters
`cmd` **`string`**
`runnerName` **`string?`**
</div>
<hr>
<div id='get'>
<h4 class='heading'>
hilbish.runner.get(name)
<a href="#get" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Get a runner by name.
#### Parameters
`name` **`string`**
Name of the runner to retrieve.
</div>
<hr>
<div id='getCurrent'>
<h4 class='heading'>
hilbish.runner.getCurrent()
<a href="#getCurrent" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns the current runner by name.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='run'>
<h4 class='heading'>
hilbish.runner.run(input, priv)
<a href="#run" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Runs `input` with the currently set Hilbish runner.
This method is how Hilbish executes commands.
`priv` is an optional boolean used to state if the input should be saved to history.
#### Parameters
`input` **`string`**
`priv` **`bool`**
</div>
<hr>
<div id='set'>
<h4 class='heading'>
hilbish.runner.set(name, runner)
<a href="#set" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
*Sets* a runner by name. The difference between this function and
add, is set will *not* check if the named runner exists.
The runner table must have the run function in it.
#### Parameters
`name` **`string`**
`runner` **`table`**
</div>
<hr>
<div id='setCurrent'>
<h4 class='heading'>
hilbish.runner.setCurrent(name)
<a href="#setCurrent" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sets Hilbish's runner mode by name.
#### Parameters
`name` **`string`**
</div>
<hr>
<div id='setMode'>
<h4 class='heading'>
hilbish.runner.setMode(mode)
<a href="#setMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
**NOTE: This function is deprecated and will be removed in 3.0**
Use `hilbish.runner.setCurrent` instead.
This is the same as the `hilbish.runnerMode` function.
It takes a callback, which will be used to execute all interactive input.
Or a string which names the runner mode to use.
#### Parameters
`mode` **`string|function`**
</div>
<hr>
<div id='sh'>
<h4 class='heading'>
hilbish.runner.sh()
<a href="#sh" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
#### Parameters
This function has no parameters.
</div>

View File

@ -1,66 +0,0 @@
---
title: Module readline
description: line reader library
layout: doc
menu:
docs:
parent: "API"
---
## Introduction
The readline module is responsible for reading input from the user.
The readline module is what Hilbish uses to read input from the user,
including all the interactive features of Hilbish like history search,
syntax highlighting, everything. The global Hilbish readline instance
is usable at `hilbish.editor`.
## Functions
|||
|----|----|
|<a href="#New">new() -> @Readline</a>|Creates a new readline instance.|
<hr>
<div id='New'>
<h4 class='heading'>
readline.new() -> <a href="/Hilbish/docs/api/readline/#readline" style="text-decoration: none;" id="lol">Readline</a>
<a href="#New" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Creates a new readline instance.
#### Parameters
This function has no parameters.
</div>
## Types
<hr>
## Readline
### Methods
#### deleteByAmount(amount)
Deletes characters in the line by the given amount.
#### getLine() -> string
Returns the current input line.
#### getVimRegister(register) -> string
Returns the text that is at the register.
#### insert(text)
Inserts text into the Hilbish command line.
#### log(text)
Prints a message *before* the prompt without it being interrupted by user input.
#### read() -> string
Reads input from the user.
#### getChar() -> string
Reads a keystroke from the user. This is in a format of something like Ctrl-L.
#### setVimRegister(register, text)
Sets the vim register at `register` to hold the passed text.

View File

@ -1,50 +0,0 @@
---
title: Module snail
description: shell script interpreter library
layout: doc
menu:
docs:
parent: "API"
---
## Introduction
The snail library houses Hilbish's Lua wrapper of its shell script interpreter.
It's not very useful other than running shell scripts, which can be done with other
Hilbish functions.
## Functions
|||
|----|----|
|<a href="#new">new() -> @Snail</a>|Creates a new Snail instance.|
<hr>
<div id='new'>
<h4 class='heading'>
snail.new() -> <a href="/Hilbish/docs/api/snail/#snail" style="text-decoration: none;" id="lol">Snail</a>
<a href="#new" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Creates a new Snail instance.
#### Parameters
This function has no parameters.
</div>
## Types
<hr>
## Snail
A Snail is a shell script interpreter instance.
### Methods
#### dir(path)
Changes the directory of the snail instance.
The interpreter keeps its set directory even when the Hilbish process changes
directory, so this should be called on the `hilbish.cd` hook.
#### run(command, streams)
Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams.

View File

@ -1,51 +0,0 @@
---
title: Module yarn
description: multi threading library
layout: doc
menu:
docs:
parent: "API"
---
## Introduction
Yarn is a simple multithreading library. Threads are individual Lua states,
so they do NOT share the same environment as the code that runs the thread.
Bait and Commanders are shared though, so you *can* throw hooks from 1 thread to another.
Example:
```lua
local yarn = require 'yarn'
-- calling t will run the yarn thread.
local t = yarn.thread(print)
t 'printing from another lua state!'
```
## Functions
|||
|----|----|
|<a href="#thread">thread(fun) -> @Thread</a>|Creates a new, fresh Yarn thread.|
<hr>
<div id='thread'>
<h4 class='heading'>
yarn.thread(fun) -> <a href="/Hilbish/docs/api/yarn/#thread" style="text-decoration: none;" id="lol">Thread</a>
<a href="#thread" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Creates a new, fresh Yarn thread.
`fun` is the function that will run in the thread.
#### Parameters
This function has no parameters.
</div>
## Types
<hr>
## Thread
### Methods

View File

@ -56,50 +56,6 @@ return {cg, cg2}, prefix
Which looks like this:
{{< video src="https://safe.saya.moe/t4CiLK6dgPbD.mp4" >}}
# Completion Group Types
### grid
Grid is the simplest completion group type. All items are strings and when
completion is done is displayed in a grid based on size.
Example:
```lua
{
items = {'just', 'a bunch', 'of items', 'here', 'hehe'},
type = 'grid'
}
```
### list
The list completion group type displays in a list. A list item can either be a string, or a table for additional display options.
A completion alias can be specified either as the `2nd` entry in the options table
or te `alias` key.
A description can optionally be displayed for a list item, which is either the `1st`
entry or the `description` key.
Lastly, list entries can be styled. This is done with the `display` key. If this is present, this
overrides what the completion item *looks* like.
Example:
```lua
{
items = {
['--flag'] = {
description = 'this flag nukes the bri ish',
alias = '--bye-bri-ish',
display = lunacolors.format('--{blue}fl{red}ag')
},
['--flag2'] = {
'make pizza', -- description
'--pizzuh', -- alias
display = lunacolors.yellow '--pizzuh'
},
'--flag3'
},
type = 'list'
}
```
# Completion Handler
Like most parts of Hilbish, it's made to be extensible and
customizable. The default handler for completions in general can

View File

@ -76,8 +76,3 @@ of an exact match.
#### Default: `true`
If this is enabled, when a background job is finished,
a [notification](../notifications) will be sent.
### `processorSkipList`
#### Value: `table`
#### Default: `{}`
A table listing the names of command processors to skip.

View File

@ -33,6 +33,19 @@ needs to run interactive input. For more detail, see the [API documentation](../
The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode`
and also provides the shell script and Lua runner functions that Hilbish itself uses.
A runner function is expected to return a table with the following values:
- `exitCode` (number): Exit code of the command
- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case
more is requested.
- `err` (string): A string that represents an error from the runner.
This should only be set when, for example, there is a syntax error.
It can be set to a few special values for Hilbish to throw the right
hooks and have a better looking message.
- `<command>: not-found` will throw a `command.not-found` hook
based on what `<command>` is.
- `<command>: not-executable` will throw a `command.not-executable` hook.
- `continue` (boolean): Whether Hilbish should prompt the user for no input
## Functions
These are the "low level" functions for the `hilbish.runner` interface.

View File

@ -53,39 +53,8 @@ which follows XDG on Linux and MacOS, and is located in %APPDATA% on Windows.
As the directory is usually `~/.config` on Linux, you can run this command to copy it:
`cp /usr/share/hilbish/.hilbishrc.lua ~/.config/hilbish/init.lua`
Now we can get to customization!
If we closely examine a small snippet of the default config:
```lua
-- Default Hilbish config
-- .. with some omitted code .. --
local function doPrompt(fail)
hilbish.prompt(lunacolors.format(
'{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆ '
))
end
doPrompt()
bait.catch('command.exit', function(code)
doPrompt(code ~= 0)
end)
```
We see a whopping **three** Hilbish libraries being used in this part of code.
First is of course, named after the shell itself, [`hilbish`](../api/hilbish). This is kind of a
"catch-all" namespace for functions that directly related to shell functionality/settings.
And as we can see, the [hilbish.prompt](../api/hilbish/#prompt) function is used
to change our prompt. Change our prompt to what, exactly?
The doc for the function states that the verbs `%u` and `%d`are used for username and current directory
of the shell, respectively.
We wrap this in the [`lunacolors.format`](../lunacolors) function, to give
our prompt some nice color.
But you might have also noticed that this is in the `doPrompt` function, which is called once,
and then used again in a [bait](../api/bait) hook. Specifically, the `command.exit` hook,
which is called after a command exits, so when it finishes running.
Now you can get to editing it. Since it's just a Lua file, having basic
knowledge of Lua would help. All of Lua's standard libraries and functions
from Lua 5.4 are available. Hilbish has some custom and modules that are
available. To see them, you can run the `doc` command. This also works as
general documentation for other things.

View File

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

View File

@ -28,17 +28,14 @@ Colors:
- magenta
- cyan
- white
Styles:
- reset
- bold
- dim
- italic
- underline
- invert
For the colors, there are background and bright variants. Background color
variants have a `Bg` suffix, while bright variants use the `bright` prefix.
These can also be combined. Note that appropriate camel casing must be applied.
For example, bright blue would be written as `brightBlue`, a cyan background as
`cyanBg`, and combining them would result in `brightBlueBg`.
For the colors, there are background and bright variants. The background
color variants have a suffix of `Bg` and bright has a prefix of `bright`.
Note that appropriate camel casing has to be applied to them. So bright
blue would be `brightBlue` and background cyan would be `cyanBg`.

View File

@ -1,25 +1,40 @@
---
title: Module dirs
description: internal directory management
description: No description.
layout: doc
menu:
docs:
parent: "Nature"
---
<hr>
<div id='setOld'>
<h4 class='heading'>
dirs.setOld(d)
<a href="#setOld" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
## Introduction
The dirs module defines a small set of functions to store and manage
directories.
Sets the old directory string.
#### Parameters
`d` **`string`**
</div>
<hr>
<div id='push'>
<h4 class='heading'>
dirs.push()
<a href="#push" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Add `d` to the recent directories list.
#### Parameters
This function has no parameters.
</div>
## Functions
|||
|----|----|
|<a href="#setOld">setOld(d)</a>|Sets the old directory string.|
|<a href="#recent">recent(idx)</a>|Get entry from recent directories list based on index.|
|<a href="#push">push(dir)</a>|Add `dir` to the recent directories list.|
|<a href="#pop">pop(num)</a>|Remove the specified amount of dirs from the recent directories list.|
|<a href="#peak">peak(num)</a>|Look at `num` amount of recent directories, starting from the latest.|
<hr>
<div id='peak'>
<h4 class='heading'>
@ -30,11 +45,8 @@ dirs.peak(num)
</h4>
Look at `num` amount of recent directories, starting from the latest.
This returns a table of recent directories, up to the `num` amount.
#### Parameters
`num` **`number`**
</div>
<hr>
@ -49,24 +61,6 @@ dirs.pop(num)
Remove the specified amount of dirs from the recent directories list.
#### Parameters
`num` **`number`**
</div>
<hr>
<div id='push'>
<h4 class='heading'>
dirs.push(dir)
<a href="#push" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Add `dir` to the recent directories list.
#### Parameters
`dir` **`string`**
</div>
<hr>
@ -81,23 +75,5 @@ dirs.recent(idx)
Get entry from recent directories list based on index.
#### Parameters
`idx` **`number`**
</div>
<hr>
<div id='setOld'>
<h4 class='heading'>
dirs.setOld(d)
<a href="#setOld" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sets the old directory string.
#### Parameters
`d` **`string`**
</div>

View File

@ -1,76 +0,0 @@
---
title: Module doc
description: command-line doc rendering
layout: doc
menu:
docs:
parent: "Nature"
---
## Introduction
The doc module contains a small set of functions
used by the Greenhouse pager to render parts of the documentation pages.
This is only documented for the sake of it. It's only intended use
is by the Greenhouse pager.
## Functions
|||
|----|----|
|<a href="#renderInfoBlock">renderInfoBlock(type, text)</a>|Renders an info block. An info block is a block of text with|
|<a href="#renderCodeBlock">renderCodeBlock(text)</a>|Assembles and renders a code block. This returns|
|<a href="#highlight">highlight(text)</a>|Performs basic Lua code highlighting.|
<hr>
<div id='highlight'>
<h4 class='heading'>
doc.highlight(text)
<a href="#highlight" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Performs basic Lua code highlighting.
#### Parameters
`text` **`string`**
Code/text to do highlighting on.
</div>
<hr>
<div id='renderCodeBlock'>
<h4 class='heading'>
doc.renderCodeBlock(text)
<a href="#renderCodeBlock" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Assembles and renders a code block. This returns
the supplied text based on the number of command line columns,
and styles it to resemble a code block.
#### Parameters
`text` **`string`**
</div>
<hr>
<div id='renderInfoBlock'>
<h4 class='heading'>
doc.renderInfoBlock(type, text)
<a href="#renderInfoBlock" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Renders an info block. An info block is a block of text with
an icon and styled text block.
#### Parameters
`type` **`string`**
Type of info block. The only one specially styled is the `warning`.
`text` **`string`**
</div>

108
editor.go Normal file
View File

@ -0,0 +1,108 @@
package main
import (
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
// #interface editor
// interactions for Hilbish's line reader
// The hilbish.editor interface provides functions to
// directly interact with the line editor in use.
func editorLoader(rtm *rt.Runtime) *rt.Table {
exports := map[string]util.LuaExport{
"insert": {editorInsert, 1, false},
"setVimRegister": {editorSetRegister, 1, false},
"getVimRegister": {editorGetRegister, 2, false},
"getLine": {editorGetLine, 0, false},
"readChar": {editorReadChar, 0, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
return mod
}
// #interface editor
// insert(text)
// Inserts text into the Hilbish command line.
// #param text string
func editorInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
text, err := c.StringArg(0)
if err != nil {
return nil, err
}
lr.rl.Insert(text)
return c.Next(), nil
}
// #interface editor
// setVimRegister(register, text)
// Sets the vim register at `register` to hold the passed text.
// #aram register string
// #param text string
func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
register, err := c.StringArg(0)
if err != nil {
return nil, err
}
text, err := c.StringArg(1)
if err != nil {
return nil, err
}
lr.rl.SetRegisterBuf(register, []rune(text))
return c.Next(), nil
}
// #interface editor
// getVimRegister(register) -> string
// Returns the text that is at the register.
// #param register string
func editorGetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
register, err := c.StringArg(0)
if err != nil {
return nil, err
}
buf := lr.rl.GetFromRegister(register)
return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil
}
// #interface editor
// getLine() -> string
// Returns the current input line.
// #returns string
func editorGetLine(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
buf := lr.rl.GetLine()
return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil
}
// #interface editor
// getChar() -> string
// Reads a keystroke from the user. This is in a format of something like Ctrl-L.
func editorReadChar(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
buf := lr.rl.ReadChar()
return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil
}

View File

@ -7,6 +7,27 @@ local hilbish = {}
--- @param cmd string
function hilbish.aliases.add(alias, cmd) end
--- This is the same as the `hilbish.runnerMode` function.
--- It takes a callback, which will be used to execute all interactive input.
--- In normal cases, neither callbacks should be overrided by the user,
--- as the higher level functions listed below this will handle it.
function hilbish.runner.setMode(cb) end
--- Returns the current input line.
function hilbish.editor.getLine() end
--- Returns the text that is at the register.
function hilbish.editor.getVimRegister(register) end
--- Inserts text into the Hilbish command line.
function hilbish.editor.insert(text) end
--- Reads a keystroke from the user. This is in a format of something like Ctrl-L.
function hilbish.editor.getChar() end
--- Sets the vim register at `register` to hold the passed text.
function hilbish.editor.setVimRegister(register, text) end
--- Return binaries/executables based on the provided parameters.
--- This function is meant to be used as a helper in a command completion handler.
---
@ -110,6 +131,24 @@ function hilbish.prompt(str, typ) end
--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
function hilbish.read(prompt) end
--- Runs `cmd` in Hilbish's shell script interpreter.
--- The `streams` parameter specifies the output and input streams the command should use.
--- For example, to write command output to a sink.
--- As a table, the caller can directly specify the standard output, error, and input
--- streams of the command with the table keys `out`, `err`, and `input` respectively.
--- As a boolean, it specifies whether the command should use standard output or return its output streams.
---
function hilbish.run(cmd, streams) 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.
--- Read [about runner mode](../features/runner-mode) for more information.
function hilbish.runnerMode(mode) end
--- Executed the `cb` function after a period of `time`.
--- This creates a Timer that starts ticking immediately.
function hilbish.timeout(cb, time) end
@ -129,6 +168,28 @@ function hilbish.jobs:foreground() end
--- or `load`, but is appropriated for the runner interface.
function hilbish.runner.lua(cmd) end
--- Sets/toggles the option of automatically flushing output.
--- A call with no argument will toggle the value.
--- @param auto boolean|nil
function hilbish:autoFlush(auto) end
--- Flush writes all buffered input to the sink.
function hilbish:flush() end
--- Reads a liine of input from the sink.
--- @returns string
function hilbish:read() end
--- Reads all input from the sink.
--- @returns string
function hilbish:readAll() end
--- Writes data to a sink.
function hilbish:write(str) end
--- Writes data to a sink with a newline at the end.
function hilbish:writeln(str) end
--- Starts running the job.
function hilbish.jobs:start() end
@ -139,6 +200,10 @@ function hilbish.jobs:stop() end
--- It will throw if any error occurs.
function hilbish.module.load(path) end
--- Runs a command in Hilbish's shell script interpreter.
--- This is the equivalent of using `source`.
function hilbish.runner.sh(cmd) end
--- Starts a timer.
function hilbish.timers:start() end
@ -197,26 +262,4 @@ function hilbish.timers.create(type, time, callback) end
--- Retrieves a timer via its ID.
function hilbish.timers.get(id) end
--- Sets/toggles the option of automatically flushing output.
--- A call with no argument will toggle the value.
--- @param auto boolean|nil
function hilbish:autoFlush(auto) end
--- Flush writes all buffered input to the sink.
function hilbish:flush() end
--- Reads a liine of input from the sink.
--- @returns string
function hilbish:read() end
--- Reads all input from the sink.
--- @returns string
function hilbish:readAll() end
--- Writes data to a sink.
function hilbish:write(str) end
--- Writes data to a sink with a newline at the end.
function hilbish:writeln(str) end
return hilbish

View File

@ -1,32 +0,0 @@
--- @meta
local readline = {}
--- Deletes characters in the line by the given amount.
function readline:deleteByAmount(amount) end
--- Returns the current input line.
function readline:getLine() end
--- Returns the text that is at the register.
function readline:getVimRegister(register) end
--- Inserts text into the Hilbish command line.
function readline:insert(text) end
--- Prints a message *before* the prompt without it being interrupted by user input.
function readline:log(text) end
--- Creates a new readline instance.
function readline.new() end
--- Reads input from the user.
function readline:read() end
--- Reads a keystroke from the user. This is in a format of something like Ctrl-L.
function readline:getChar() end
--- Sets the vim register at `register` to hold the passed text.
function readline:setVimRegister(register, text) end
return readline

View File

@ -1,16 +0,0 @@
--- @meta
local snail = {}
--- Changes the directory of the snail instance.
--- The interpreter keeps its set directory even when the Hilbish process changes
--- directory, so this should be called on the `hilbish.cd` hook.
function snail:dir(path) end
--- Creates a new Snail instance.
function snail.new() end
--- Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams.
function snail:run(command, streams) end
return snail

View File

@ -1,83 +0,0 @@
--- @meta
local util = {}
---
function util.AbbrevHome changes the user's home directory in the path string to ~ (tilde) end
---
function util. end
---
function util.DoFile runs the contents of the file in the Lua runtime. end
---
function util.DoString runs the code string in the Lua runtime. end
--- directory.
function util.ExpandHome expands ~ (tilde) in the path, changing it to the user home end
---
function util. end
---
function util.ForEach loops through a Lua table. end
---
function util. end
--- a string and a closure.
function util.HandleStrCallback handles function parameters for Go functions which take end
---
function util. end
---
function util. end
---
function util.SetExports puts the Lua function exports in the table. end
--- It is accessible via the __docProp metatable. It is a table of the names of the fields.
function util.SetField sets a field in a table, adding docs for it. end
--- is one which has a metatable proxy to ensure no overrides happen to it.
--- It sets the field in the table and sets the __docProp metatable on the
--- user facing table.
function util.SetFieldProtected sets a field in a protected table. A protected table end
--- Sets/toggles the option of automatically flushing output.
--- A call with no argument will toggle the value.
--- @param auto boolean|nil
function util:autoFlush(auto) end
--- Flush writes all buffered input to the sink.
function util:flush() end
---
function util. end
--- Reads a liine of input from the sink.
--- @returns string
function util:read() end
--- Reads all input from the sink.
--- @returns string
function util:readAll() end
--- Writes data to a sink.
function util:write(str) end
--- Writes data to a sink with a newline at the end.
function util:writeln(str) end
---
function util. end
---
function util. end
---
function util. end
return util

View File

@ -1,9 +0,0 @@
--- @meta
local yarn = {}
--- Creates a new, fresh Yarn thread.
--- `fun` is the function that will run in the thread.
function yarn.thread(fun) end
return yarn

549
exec.go
View File

@ -1,26 +1,208 @@
package main
import (
"bytes"
"context"
"errors"
"os/exec"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"mvdan.cc/sh/v3/shell"
//"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
)
var errNotExec = errors.New("not executable")
var errNotFound = errors.New("not found")
var runnerMode rt.Value = rt.NilValue
var runnerMode rt.Value = rt.StringValue("hybrid")
type streams struct {
stdout io.Writer
stderr io.Writer
stdin io.Reader
}
type execError struct{
typ string
cmd string
code int
colon bool
err error
}
func (e execError) Error() string {
return fmt.Sprintf("%s: %s", e.cmd, e.typ)
}
func (e execError) sprint() error {
sep := " "
if e.colon {
sep = ": "
}
return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error())
}
func isExecError(err error) (execError, bool) {
if exErr, ok := err.(execError); ok {
return exErr, true
}
fields := strings.Split(err.Error(), ": ")
knownTypes := []string{
"not-found",
"not-executable",
}
if len(fields) > 1 && contains(knownTypes, fields[1]) {
var colon bool
var e error
switch fields[1] {
case "not-found":
e = errNotFound
case "not-executable":
colon = true
e = errNotExec
}
return execError{
cmd: fields[0],
typ: fields[1],
colon: colon,
err: e,
}, true
}
return execError{}, false
}
func runInput(input string, priv bool) {
running = true
runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run"))
_, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv))
if err != nil {
fmt.Fprintln(os.Stderr, err)
cmdString := aliases.Resolve(input)
hooks.Emit("command.preexec", input, cmdString)
rerun:
var exitCode uint8
var err error
var cont bool
// save incase it changes while prompting (For some reason)
currentRunner := runnerMode
if currentRunner.Type() == rt.StringType {
switch currentRunner.AsString() {
case "hybrid":
_, _, err = handleLua(input)
if err == nil {
cmdFinish(0, input, priv)
return
}
input, exitCode, cont, err = handleSh(input)
case "hybridRev":
_, _, _, err = handleSh(input)
if err == nil {
cmdFinish(0, input, priv)
return
}
input, exitCode, err = handleLua(input)
case "lua":
input, exitCode, err = handleLua(input)
case "sh":
input, exitCode, cont, err = handleSh(input)
}
} else {
// can only be a string or function so
var runnerErr error
input, exitCode, cont, runnerErr, err = runLuaRunner(currentRunner, input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
cmdFinish(124, input, priv)
return
}
// yep, we only use `err` to check for lua eval error
// our actual error should only be a runner provided error at this point
// command not found type, etc
err = runnerErr
}
if cont {
input, err = reprompt(input)
if err == nil {
goto rerun
} else if err == io.EOF {
return
}
}
if err != nil {
if exErr, ok := isExecError(err); ok {
hooks.Emit("command." + exErr.typ, exErr.cmd)
} else {
fmt.Fprintln(os.Stderr, err)
}
}
cmdFinish(exitCode, input, priv)
}
func reprompt(input string) (string, error) {
for {
in, err := continuePrompt(strings.TrimSuffix(input, "\\"))
if err != nil {
lr.SetPrompt(fmtPrompt(prompt))
return input, err
}
if strings.HasSuffix(in, "\\") {
continue
}
return in, nil
}
}
func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, runnerErr, err error) {
term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 3, false)
err = rt.Call(l.MainThread(), runr, []rt.Value{rt.StringValue(userInput)}, term)
if err != nil {
return "", 124, false, nil, err
}
var runner *rt.Table
var ok bool
runnerRet := term.Get(0)
if runner, ok = runnerRet.TryTable(); !ok {
fmt.Fprintln(os.Stderr, "runner did not return a table")
exitCode = 125
input = userInput
return
}
if code, ok := runner.Get(rt.StringValue("exitCode")).TryInt(); ok {
exitCode = uint8(code)
}
if inp, ok := runner.Get(rt.StringValue("input")).TryString(); ok {
input = inp
}
if errStr, ok := runner.Get(rt.StringValue("err")).TryString(); ok {
runnerErr = fmt.Errorf("%s", errStr)
}
if c, ok := runner.Get(rt.StringValue("continue")).TryBool(); ok {
continued = c
}
return
}
func handleLua(input string) (string, uint8, error) {
@ -29,12 +211,12 @@ func handleLua(input string) (string, uint8, error) {
chunk, err := l.CompileAndLoadLuaChunk("", []byte(cmdString), rt.TableValue(l.GlobalEnv()))
if err != nil && noexecute {
fmt.Println(err)
/* if lerr, ok := err.(*lua.ApiError); ok {
if perr, ok := lerr.Cause.(*parse.Error); ok {
print(perr.Pos.Line == parse.EOF)
}
/* if lerr, ok := err.(*lua.ApiError); ok {
if perr, ok := lerr.Cause.(*parse.Error); ok {
print(perr.Pos.Line == parse.EOF)
}
*/
}
*/
return cmdString, 125, err
}
// And if there's no syntax errors and -n isnt provided, run
@ -50,13 +232,336 @@ func handleLua(input string) (string, uint8, error) {
return cmdString, 125, err
}
func handleSh(cmdString string) (input string, exitCode uint8, cont bool, runErr error) {
shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh"))
var err error
input, exitCode, cont, runErr, err = runLuaRunner(shRunner, cmdString)
if err != nil {
runErr = err
}
return
}
func execSh(cmdString string) (string, uint8, bool, error) {
_, _, err := execCommand(cmdString, nil)
if err != nil {
// If input is incomplete, start multiline prompting
if syntax.IsIncomplete(err) {
if !interactive {
return cmdString, 126, false, err
}
return cmdString, 126, true, err
} else {
if code, ok := interp.IsExitStatus(err); ok {
return cmdString, code, false, nil
} else {
return cmdString, 126, false, err
}
}
}
return cmdString, 0, false, nil
}
// Run command in sh interpreter
func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return nil, nil, err
}
if strms == nil {
strms = &streams{}
}
if strms.stdout == nil {
strms.stdout = os.Stdout
}
if strms.stderr == nil {
strms.stderr = os.Stderr
}
if strms.stdin == nil {
strms.stdin = os.Stdin
}
interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner)
buf := new(bytes.Buffer)
printer := syntax.NewPrinter()
var bg bool
for _, stmt := range file.Stmts {
bg = false
if stmt.Background {
bg = true
printer.Print(buf, stmt.Cmd)
stmtStr := buf.String()
buf.Reset()
jobs.add(stmtStr, []string{}, "")
}
interp.ExecHandler(execHandle(bg))(runner)
err = runner.Run(context.TODO(), stmt)
if err != nil {
return strms.stdout, strms.stderr, err
}
}
return strms.stdout, strms.stderr, nil
}
func execHandle(bg bool) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " "))
// i dont really like this but it works
if aliases.All()[args[0]] != "" {
for i, arg := range args {
if strings.Contains(arg, " ") {
args[i] = fmt.Sprintf("\"%s\"", arg)
}
}
_, argstring = splitInput(strings.Join(args, " "))
// If alias was found, use command alias
argstring = aliases.Resolve(argstring)
var err error
args, err = shell.Fields(argstring, nil)
if err != nil {
return err
}
}
// If command is defined in Lua then run it
luacmdArgs := rt.NewTable()
for i, str := range args[1:] {
luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
}
hc := interp.HandlerCtx(ctx)
if cmd := cmds.Commands[args[0]]; cmd != nil {
stdin := newSinkInput(hc.Stdin)
stdout := newSinkOutput(hc.Stdout)
stderr := newSinkOutput(hc.Stderr)
sinks := rt.NewTable()
sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud))
sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud))
sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud))
sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud))
t := rt.NewThread(l)
sig := make(chan os.Signal)
exit := make(chan bool)
luaexitcode := rt.IntValue(63)
var err error
go func() {
defer func() {
if r := recover(); r != nil {
exit <- true
}
}()
signal.Notify(sig, os.Interrupt)
select {
case <-sig:
t.KillContext()
return
}
}()
go func() {
luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
exit <- true
}()
<-exit
if err != nil {
fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
return interp.NewExitStatus(1)
}
var exitcode uint8
if code, ok := luaexitcode.TryInt(); ok {
exitcode = uint8(code)
} else if luaexitcode != rt.NilValue {
// deregister commander
delete(cmds.Commands, args[0])
fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
}
return interp.NewExitStatus(exitcode)
}
err := lookpath(args[0])
if err == errNotExec {
return execError{
typ: "not-executable",
cmd: args[0],
code: 126,
colon: true,
err: errNotExec,
}
} else if err != nil {
return execError{
typ: "not-found",
cmd: args[0],
code: 127,
err: errNotFound,
}
}
killTimeout := 2 * time.Second
// from here is basically copy-paste of the default exec handler from
// sh/interp but with our job handling
path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
if err != nil {
fmt.Fprintln(hc.Stderr, err)
return interp.NewExitStatus(127)
}
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,
}
var j *job
if bg {
j = jobs.getLatest()
j.setHandle(&cmd)
err = j.start()
} else {
err = cmd.Start()
}
if err == nil {
if done := ctx.Done(); done != nil {
go func() {
<-done
if killTimeout <= 0 || runtime.GOOS == "windows" {
cmd.Process.Signal(os.Kill)
return
}
// TODO: don't temporarily leak this goroutine
// if the program stops itself with the
// interrupt.
go func() {
time.Sleep(killTimeout)
cmd.Process.Signal(os.Kill)
}()
cmd.Process.Signal(os.Interrupt)
}()
}
err = cmd.Wait()
}
exit := handleExecErr(err)
if bg {
j.exitCode = int(exit)
j.finish()
}
return interp.NewExitStatus(exit)
}
}
func handleExecErr(err error) (exit uint8) {
ctx := context.TODO()
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return
}
exit = uint8(128 + status.Signal())
return
}
exit = uint8(status.ExitStatus())
return
}
exit = 1
return
case *exec.Error:
// did not start
//fmt.Fprintf(hc.Stderr, "%v\n", err)
exit = 127
default: return
}
return
}
func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable
var skip []string
if runtime.GOOS == "windows" {
skip = []string{"./", "../", "~/", "C:"}
} else {
skip = []string{"./", "/", "../", "~/"}
}
for _, s := range skip {
if strings.HasPrefix(file, s) {
return findExecutable(file, false, false)
}
}
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
path := filepath.Join(dir, file)
err := findExecutable(path, true, false)
if err == errNotExec {
return err
} else if err == nil {
return nil
}
}
return os.ErrNotExist
}
func splitInput(input string) ([]string, string) {
// end my suffering
// TODO: refactor this garbage
quoted := false
startlastcmd := false
lastcmddone := false
cmdArgs := []string{}
sb := &strings.Builder{}
cmdstr := &strings.Builder{}
lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1)
for _, r := range input {
if r == '"' {
@ -72,6 +577,22 @@ func splitInput(input string) ([]string, string) {
// if not quoted and there's a space then add to cmdargs
cmdArgs = append(cmdArgs, sb.String())
sb.Reset()
} else if !quoted && r == '^' && startlastcmd && !lastcmddone {
// if ^ is found, isnt in quotes and is
// the second occurence of the character and is
// the first time "^^" has been used
cmdstr.WriteString(lastcmd)
sb.WriteString(lastcmd)
startlastcmd = !startlastcmd
lastcmddone = !lastcmddone
continue
} else if !quoted && r == '^' && !lastcmddone {
// if ^ is found, isnt in quotes and is the
// first time of starting "^^"
startlastcmd = !startlastcmd
continue
} else {
sb.WriteRune(r)
}
@ -83,3 +604,11 @@ func splitInput(input string) ([]string, string) {
return cmdArgs, cmdstr.String()
}
func cmdFinish(code uint8, cmdstr string, private bool) {
util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)))
// 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.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private)
}

View File

@ -1,12 +1,17 @@
//go:build unix
package util
package main
import (
"os"
"syscall"
)
func FindExecutable(path string, inPath, dirs bool) error {
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
func findExecutable(path string, inPath, dirs bool) error {
f, err := os.Stat(path)
if err != nil {
return err
@ -20,5 +25,5 @@ func FindExecutable(path string, inPath, dirs bool) error {
return nil
}
}
return ErrNotExec
return errNotExec
}

View File

@ -1,13 +1,18 @@
//go:build windows
package util
package main
import (
"path/filepath"
"os"
"syscall"
)
func FindExecutable(path string, inPath, dirs bool) error {
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
func findExecutable(path string, inPath, dirs bool) error {
nameExt := filepath.Ext(path)
pathExts := filepath.SplitList(os.Getenv("PATHEXT"))
if inPath {
@ -21,15 +26,15 @@ func FindExecutable(path string, inPath, dirs bool) error {
} else {
_, err := os.Stat(path)
if err == nil {
if Contains(pathExts, nameExt) { return nil }
return ErrNotExec
if contains(pathExts, nameExt) { return nil }
return errNotExec
}
}
} else {
_, err := os.Stat(path)
if err == nil {
if Contains(pathExts, nameExt) { return nil }
return ErrNotExec
if contains(pathExts, nameExt) { return nil }
return errNotExec
}
}

6
go.mod
View File

@ -28,10 +28,10 @@ require (
golang.org/x/text v0.14.0 // indirect
)
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd
replace github.com/maxlandon/readline => ./golibs/readline
replace github.com/maxlandon/readline => ./readline
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-20241104031959-5551ea280f23
replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749

8
go.sum
View File

@ -1,7 +1,7 @@
github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23 h1:mUZnT0gmDEmTkqXsbnDbuJ3CNil7DCOMiCQYgjbKIdI=
github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73 h1:zTTUJqNnrF2qf4LgygN8Oae5Uxn6ewH0hA8jyTCHfXw=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73/go.mod h1:YZalN5H7WNQw3DGij6IvHsEhn5YMW7M2FCwG6gnfKy4=
github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749 h1:jIFnWBTsYw8s7RX7H2AOXjDVhWP3ol7OzUVaPN2KnGI=
github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd h1:THNle0FR2g7DMO1y3Bx1Zr7rYeiLXt3st3UkxEsMzL4=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd/go.mod h1:YZalN5H7WNQw3DGij6IvHsEhn5YMW7M2FCwG6gnfKy4=
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/arnodel/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw=

View File

@ -47,7 +47,7 @@ type Recoverer func(event string, handler *Listener, err interface{})
type Listener struct{
typ listenerType
once bool
caller func(...interface{}) rt.Value
caller func(...interface{})
luaCaller *rt.Closure
}
@ -73,11 +73,10 @@ func New(rtm *rt.Runtime) *Bait {
}
// Emit throws an event.
func (b *Bait) Emit(event string, args ...interface{}) []rt.Value {
var returns []rt.Value
func (b *Bait) Emit(event string, args ...interface{}) {
handles := b.handlers[event]
if handles == nil {
return nil
return
}
for idx, handle := range handles {
@ -98,37 +97,28 @@ func (b *Bait) Emit(event string, args ...interface{}) []rt.Value {
}
luaArgs = append(luaArgs, luarg)
}
luaRet, err := rt.Call1(b.rtm.MainThread(), funcVal, luaArgs...)
_, err := rt.Call1(b.rtm.MainThread(), funcVal, luaArgs...)
if err != nil {
if event != "error" {
b.Emit("error", event, handle.luaCaller, err.Error())
return nil
return
}
// if there is an error in an error event handler, panic instead
// (calls the go recoverer function)
panic(err)
}
if luaRet != rt.NilValue {
returns = append(returns, luaRet)
}
} else {
ret := handle.caller(args...)
if ret != rt.NilValue {
returns = append(returns, ret)
}
handle.caller(args...)
}
if handle.once {
b.removeListener(event, idx)
}
}
return returns
}
// On adds a Go function handler for an event.
func (b *Bait) On(event string, handler func(...interface{}) rt.Value) *Listener {
func (b *Bait) On(event string, handler func(...interface{})) *Listener {
listener := &Listener{
typ: goListener,
caller: handler,
@ -140,7 +130,7 @@ func (b *Bait) On(event string, handler func(...interface{}) rt.Value) *Listener
// OnLua adds a Lua function handler for an event.
func (b *Bait) OnLua(event string, handler *rt.Closure) *Listener {
listener := &Listener{
listener :=&Listener{
typ: luaListener,
luaCaller: handler,
}
@ -172,7 +162,7 @@ func (b *Bait) OffLua(event string, handler *rt.Closure) {
}
// Once adds a Go function listener for an event that only runs once.
func (b *Bait) Once(event string, handler func(...interface{}) rt.Value) *Listener {
func (b *Bait) Once(event string, handler func(...interface{})) *Listener {
listener := &Listener{
typ: goListener,
once: true,
@ -236,6 +226,27 @@ func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
return rt.TableValue(mod), nil
}
func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, args ...interface{}) {
funcVal := rt.FunctionValue(catcher)
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)
}
}
// catch(name, cb)
// Catches an event. This function can be used to act on events.
// #param name string The name of the hook.
@ -359,7 +370,7 @@ func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
for i, v := range c.Etc() {
ifaceSlice[i] = v
}
ret := b.Emit(name, ifaceSlice...)
b.Emit(name, ifaceSlice...)
return c.PushingNext(t.Runtime, ret...), nil
return c.Next(), nil
}

View File

@ -19,25 +19,38 @@ import (
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/arnodel/golua/lib/iolib"
"mvdan.cc/sh/v3/interp"
)
var Loader = packagelib.Loader{
Load: loaderFunc,
Name: "fs",
type fs struct{
runner *interp.Runner
Loader packagelib.Loader
}
func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
func New(runner *interp.Runner) *fs {
f := &fs{
runner: runner,
}
f.Loader = packagelib.Loader{
Load: f.loaderFunc,
Name: "fs",
}
return f
}
func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
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},
"abs": util.LuaExport{fabs, 1, false},
"basename": util.LuaExport{fbasename, 1, false},
"dir": util.LuaExport{fdir, 1, false},
"glob": util.LuaExport{fglob, 1, false},
"join": util.LuaExport{fjoin, 0, true},
"pipe": util.LuaExport{fpipe, 0, false},
"cd": util.LuaExport{f.fcd, 1, false},
"mkdir": util.LuaExport{f.fmkdir, 2, false},
"stat": util.LuaExport{f.fstat, 1, false},
"readdir": util.LuaExport{f.freaddir, 1, false},
"abs": util.LuaExport{f.fabs, 1, false},
"basename": util.LuaExport{f.fbasename, 1, false},
"dir": util.LuaExport{f.fdir, 1, false},
"glob": util.LuaExport{f.fglob, 1, false},
"join": util.LuaExport{f.fjoin, 0, true},
"pipe": util.LuaExport{f.fpipe, 0, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
@ -52,7 +65,7 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
// This can be used to resolve short paths like `..` to `/home/user`.
// #param path string
// #returns string
func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
path, err := c.StringArg(0)
if err != nil {
return nil, err
@ -72,7 +85,7 @@ func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// `.` will be returned.
// #param path string Path to get the base name of.
// #returns string
func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
@ -87,7 +100,7 @@ func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// cd(dir)
// Changes Hilbish's directory to `dir`.
// #param dir string Path to change directory to.
func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
@ -96,22 +109,13 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return nil, err
}
path = util.ExpandHome(strings.TrimSpace(path))
oldWd, _ := os.Getwd()
abspath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
abspath, _ := filepath.Abs(path)
err = os.Chdir(path)
if err != nil {
return nil, err
}
util.DoString(t.Runtime, fmt.Sprintf(`
local bait = require 'bait'
bait.throw('hilbish.cd', '%s', '%s')
`, abspath, oldWd))
interp.Dir(abspath)(f.runner)
return c.Next(), err
}
@ -121,7 +125,7 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// `~/Documents/doc.txt` then this function will return `~/Documents`.
// #param path string Path to get the directory for.
// #returns string
func fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
@ -152,7 +156,7 @@ print(matches)
-- -> {'init.lua', 'code.lua'}
#example
*/
func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
@ -186,7 +190,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish'))
-- -> '/home/user/.config/hilbish' on Linux
#example
*/
func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
strs := make([]string, len(c.Etc()))
for i, v := range c.Etc() {
if v.Type() != rt.StringType {
@ -213,7 +217,7 @@ func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
fs.mkdir('./foo/bar', true)
#example
*/
func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
@ -244,7 +248,7 @@ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// The type returned is a Lua file, same as returned from `io` functions.
// #returns File
// #returns File
func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
rf, wf, err := os.Pipe()
if err != nil {
return nil, err
@ -259,7 +263,7 @@ func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// Returns a list of all files and directories in the provided path.
// #param dir string
// #returns table
func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
@ -307,7 +311,7 @@ Would print the following:
]]--
#example
*/
func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}

View File

@ -1,276 +0,0 @@
// line reader library
// The readline module is responsible for reading input from the user.
// The readline module is what Hilbish uses to read input from the user,
// including all the interactive features of Hilbish like history search,
// syntax highlighting, everything. The global Hilbish readline instance
// is usable at `hilbish.editor`.
package readline
import (
"fmt"
"io"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
)
var rlMetaKey = rt.StringValue("__readline")
func (rl *Readline) luaLoader(rtm *rt.Runtime) (rt.Value, func()) {
rlMethods := rt.NewTable()
rlMethodss := map[string]util.LuaExport{
"deleteByAmount": {rlDeleteByAmount, 2, false},
"getLine": {rlGetLine, 1, false},
"getVimRegister": {rlGetRegister, 2, false},
"insert": {rlInsert, 2, false},
"read": {rlRead, 1, false},
"readChar": {rlReadChar, 1, false},
"setVimRegister": {rlSetRegister, 3, false},
"log": {rlLog, 2, false},
}
util.SetExports(rtm, rlMethods, rlMethodss)
rlMeta := rt.NewTable()
rlIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
_, err := rlArg(c, 0)
if err != nil {
return nil, err
}
arg := c.Arg(1)
val := rlMethods.Get(arg)
return c.PushingNext1(t.Runtime, val), nil
}
rlMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(rlIndex, "__index", 2, false)))
rtm.SetRegistry(rlMetaKey, rt.TableValue(rlMeta))
rlFuncs := map[string]util.LuaExport{
"new": {rlNew, 0, false},
}
luaRl := rt.NewTable()
util.SetExports(rtm, luaRl, rlFuncs)
return rt.TableValue(luaRl), nil
}
// new() -> @Readline
// Creates a new readline instance.
func rlNew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
rl := NewInstance()
ud := rlUserData(t.Runtime, rl)
return c.PushingNext1(t.Runtime, rt.UserDataValue(ud)), nil
}
// #member
// insert(text)
// Inserts text into the Hilbish command line.
// #param text string
func rlInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
text, err := c.StringArg(1)
if err != nil {
return nil, err
}
rl.insert([]rune(text))
return c.Next(), nil
}
// #member
// read() -> string
// Reads input from the user.
func rlRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
inp, err := rl.Readline()
if err == EOF {
fmt.Println("")
return nil, io.EOF
} else if err != nil {
return nil, err
}
return c.PushingNext1(t.Runtime, rt.StringValue(inp)), nil
}
// #member
// setVimRegister(register, text)
// Sets the vim register at `register` to hold the passed text.
// #param register string
// #param text string
func rlSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(3); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
register, err := c.StringArg(1)
if err != nil {
return nil, err
}
text, err := c.StringArg(2)
if err != nil {
return nil, err
}
rl.SetRegisterBuf(register, []rune(text))
return c.Next(), nil
}
// #member
// getVimRegister(register) -> string
// Returns the text that is at the register.
// #param register string
func rlGetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
register, err := c.StringArg(1)
if err != nil {
return nil, err
}
buf := rl.GetFromRegister(register)
return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil
}
// #member
// getLine() -> string
// Returns the current input line.
// #returns string
func rlGetLine(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
buf := rl.GetLine()
return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil
}
// #member
// getChar() -> string
// Reads a keystroke from the user. This is in a format of something like Ctrl-L.
func rlReadChar(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
buf := rl.ReadChar()
return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil
}
// #member
// deleteByAmount(amount)
// Deletes characters in the line by the given amount.
// #param amount number
func rlDeleteByAmount(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
amount, err := c.IntArg(1)
if err != nil {
return nil, err
}
rl.DeleteByAmount(int(amount))
return c.Next(), nil
}
// #member
// log(text)
// Prints a message *before* the prompt without it being interrupted by user input.
func rlLog(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
rl, err := rlArg(c, 0)
if err != nil {
return nil, err
}
logText, err := c.StringArg(1)
if err != nil {
return nil, err
}
rl.RefreshPromptLog(logText)
return c.Next(), nil
}
func rlArg(c *rt.GoCont, arg int) (*Readline, error) {
j, ok := valueToRl(c.Arg(arg))
if !ok {
return nil, fmt.Errorf("#%d must be a readline", arg+1)
}
return j, nil
}
func valueToRl(val rt.Value) (*Readline, bool) {
u, ok := val.TryUserData()
if !ok {
return nil, false
}
j, ok := u.Value().(*Readline)
return j, ok
}
func rlUserData(rtm *rt.Runtime, rl *Readline) *rt.UserData {
rlMeta := rtm.Registry(rlMetaKey)
return rt.NewUserData(rl, rlMeta.AsTable())
}

View File

@ -1,221 +0,0 @@
// shell script interpreter library
/*
The snail library houses Hilbish's Lua wrapper of its shell script interpreter.
It's not very useful other than running shell scripts, which can be done with other
Hilbish functions.
*/
package snail
import (
"errors"
"fmt"
"io"
"strings"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/arnodel/golua/lib/iolib"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
var snailMetaKey = rt.StringValue("hshsnail")
var Loader = packagelib.Loader{
Load: loaderFunc,
Name: "snail",
}
func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
snailMeta := rt.NewTable()
snailMethods := rt.NewTable()
snailFuncs := map[string]util.LuaExport{
"run": {snailrun, 3, false},
"dir": {snaildir, 2, false},
}
util.SetExports(rtm, snailMethods, snailFuncs)
snailIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
arg := c.Arg(1)
val := snailMethods.Get(arg)
return c.PushingNext1(t.Runtime, val), nil
}
snailMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(snailIndex, "__index", 2, false)))
rtm.SetRegistry(snailMetaKey, rt.TableValue(snailMeta))
exports := map[string]util.LuaExport{
"new": util.LuaExport{snailnew, 0, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
return rt.TableValue(mod), nil
}
// new() -> @Snail
// Creates a new Snail instance.
func snailnew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
s := New(t.Runtime)
return c.PushingNext1(t.Runtime, rt.UserDataValue(snailUserData(s))), nil
}
// #member
// run(command, streams)
// Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams.
// #param command string
// #param streams table
func snailrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
s, err := snailArg(c, 0)
if err != nil {
return nil, err
}
cmd, err := c.StringArg(1)
if err != nil {
return nil, err
}
streams := &util.Streams{}
thirdArg := c.Arg(2)
switch thirdArg.Type() {
case rt.TableType:
args := thirdArg.AsTable()
if luastreams, ok := args.Get(rt.StringValue("sinks")).TryTable(); ok {
handleStream(luastreams.Get(rt.StringValue("out")), streams, false, false)
handleStream(luastreams.Get(rt.StringValue("err")), streams, true, false)
handleStream(luastreams.Get(rt.StringValue("input")), streams, false, true)
}
case rt.NilType: // noop
default:
return nil, errors.New("expected 3rd arg to be a table")
}
var newline bool
var cont bool
var luaErr rt.Value = rt.NilValue
exitCode := 0
bg, _, _, err := s.Run(cmd, streams)
if err != nil {
if syntax.IsIncomplete(err) {
/*
if !interactive {
return cmdString, 126, false, false, err
}
*/
if strings.Contains(err.Error(), "unclosed here-document") {
newline = true
}
cont = true
} else {
if code, ok := interp.IsExitStatus(err); ok {
exitCode = int(code)
} else {
if exErr, ok := util.IsExecError(err); ok {
exitCode = exErr.Code
}
luaErr = rt.StringValue(err.Error())
}
}
}
runnerRet := rt.NewTable()
runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd))
runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode)))
runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont))
runnerRet.Set(rt.StringValue("newline"), rt.BoolValue(newline))
runnerRet.Set(rt.StringValue("err"), luaErr)
runnerRet.Set(rt.StringValue("bg"), rt.BoolValue(bg))
return c.PushingNext1(t.Runtime, rt.TableValue(runnerRet)), nil
}
// #member
// dir(path)
// Changes the directory of the snail instance.
// The interpreter keeps its set directory even when the Hilbish process changes
// directory, so this should be called on the `hilbish.cd` hook.
// #param path string Has to be an absolute path.
func snaildir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
s, err := snailArg(c, 0)
if err != nil {
return nil, err
}
dir, err := c.StringArg(1)
if err != nil {
return nil, err
}
interp.Dir(dir)(s.runner)
return c.Next(), nil
}
func handleStream(v rt.Value, strms *util.Streams, errStream, inStream bool) error {
if v == rt.NilValue {
return nil
}
ud, ok := v.TryUserData()
if !ok {
return errors.New("expected metatable argument")
}
val := ud.Value()
var varstrm io.ReadWriter
if f, ok := val.(*iolib.File); ok {
varstrm = f.Handle()
}
if f, ok := val.(*util.Sink); ok {
varstrm = f.Rw
}
if varstrm == nil {
return errors.New("expected either a sink or file")
}
if errStream {
strms.Stderr = varstrm
} else if inStream {
strms.Stdin = varstrm
} else {
strms.Stdout = varstrm
}
return nil
}
func snailArg(c *rt.GoCont, arg int) (*Snail, error) {
s, ok := valueToSnail(c.Arg(arg))
if !ok {
return nil, fmt.Errorf("#%d must be a snail", arg + 1)
}
return s, nil
}
func valueToSnail(val rt.Value) (*Snail, bool) {
u, ok := val.TryUserData()
if !ok {
return nil, false
}
s, ok := u.Value().(*Snail)
return s, ok
}
func snailUserData(s *Snail) *rt.UserData {
snailMeta := s.runtime.Registry(snailMetaKey)
return rt.NewUserData(s, snailMeta.AsTable())
}

View File

@ -1,304 +0,0 @@
package snail
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"mvdan.cc/sh/v3/shell"
//"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
)
// #type
// A Snail is a shell script interpreter instance.
type Snail struct{
runner *interp.Runner
runtime *rt.Runtime
}
func New(rtm *rt.Runtime) *Snail {
runner, _ := interp.New()
return &Snail{
runner: runner,
runtime: rtm,
}
}
func (s *Snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer, error){
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return false, nil, nil, err
}
if strms == nil {
strms = &util.Streams{}
}
if strms.Stdout == nil {
strms.Stdout = os.Stdout
}
if strms.Stderr == nil {
strms.Stderr = os.Stderr
}
if strms.Stdin == nil {
strms.Stdin = os.Stdin
}
interp.StdIO(strms.Stdin, strms.Stdout, strms.Stderr)(s.runner)
interp.Env(nil)(s.runner)
buf := new(bytes.Buffer)
//printer := syntax.NewPrinter()
replacer := strings.NewReplacer("[", "\\[", "]", "\\]")
var bg bool
for _, stmt := range file.Stmts {
bg = false
if stmt.Background {
bg = true
//printer.Print(buf, stmt.Cmd)
//stmtStr := buf.String()
buf.Reset()
//jobs.add(stmtStr, []string{}, "")
}
interp.ExecHandler(func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " "))
// i dont really like this but it works
aliases := make(map[string]string)
aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.list()")
util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) {
aliases[k.AsString()] = v.AsString()
})
if aliases[args[0]] != "" {
for i, arg := range args {
if strings.Contains(arg, " ") {
args[i] = fmt.Sprintf("\"%s\"", arg)
}
}
_, argstring = splitInput(strings.Join(args, " "))
// If alias was found, use command alias
argstring = util.MustDoString(s.runtime, fmt.Sprintf(`return hilbish.aliases.resolve [[%s]]`, replacer.Replace(argstring))).AsString()
var err error
args, err = shell.Fields(argstring, nil)
if err != nil {
return err
}
}
// If command is defined in Lua then run it
luacmdArgs := rt.NewTable()
for i, str := range args[1:] {
luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
}
hc := interp.HandlerCtx(ctx)
cmds := make(map[string]*rt.Closure)
luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable()
util.ForEach(luaCmds, func(k, v rt.Value) {
cmds[k.AsString()] = v.AsTable().Get(rt.StringValue("exec")).AsClosure()
})
if cmd := cmds[args[0]]; cmd != nil {
stdin := util.NewSinkInput(s.runtime, hc.Stdin)
stdout := util.NewSinkOutput(s.runtime, hc.Stdout)
stderr := util.NewSinkOutput(s.runtime, hc.Stderr)
sinks := rt.NewTable()
sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.UserData))
sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.UserData))
sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.UserData))
sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.UserData))
t := rt.NewThread(s.runtime)
sig := make(chan os.Signal)
exit := make(chan bool)
luaexitcode := rt.IntValue(63)
var err error
go func() {
defer func() {
if r := recover(); r != nil {
exit <- true
}
}()
signal.Notify(sig, os.Interrupt)
select {
case <-sig:
t.KillContext()
return
}
}()
go func() {
luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
exit <- true
}()
<-exit
if err != nil {
fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
return interp.NewExitStatus(1)
}
var exitcode uint8
if code, ok := luaexitcode.TryInt(); ok {
exitcode = uint8(code)
} else if luaexitcode != rt.NilValue {
// deregister commander
delete(cmds, args[0])
fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
}
return interp.NewExitStatus(exitcode)
}
path, err := util.LookPath(args[0])
if err == util.ErrNotExec {
return util.ExecError{
Typ: "not-executable",
Cmd: args[0],
Code: 126,
Colon: true,
Err: util.ErrNotExec,
}
} else if err != nil {
return util.ExecError{
Typ: "not-found",
Cmd: args[0],
Code: 127,
Err: util.ErrNotFound,
}
}
killTimeout := 2 * time.Second
// from here is basically copy-paste of the default exec handler from
// sh/interp but with our job handling
env := hc.Env
envList := os.Environ()
env.Each(func(name string, vr expand.Variable) bool {
if vr.Exported && vr.Kind == expand.String {
envList = append(envList, name+"="+vr.String())
}
return true
})
cmd := exec.Cmd{
Path: path,
Args: args,
Env: envList,
Dir: hc.Dir,
Stdin: hc.Stdin,
Stdout: hc.Stdout,
Stderr: hc.Stderr,
}
//var j *job
if bg {
/*
j = jobs.getLatest()
j.setHandle(&cmd)
err = j.start()
*/
} else {
err = cmd.Start()
}
if err == nil {
if done := ctx.Done(); done != nil {
go func() {
<-done
if killTimeout <= 0 || runtime.GOOS == "windows" {
cmd.Process.Signal(os.Kill)
return
}
// TODO: don't temporarily leak this goroutine
// if the program stops itself with the
// interrupt.
go func() {
time.Sleep(killTimeout)
cmd.Process.Signal(os.Kill)
}()
cmd.Process.Signal(os.Interrupt)
}()
}
err = cmd.Wait()
}
exit := util.HandleExecErr(err)
if bg {
//j.exitCode = int(exit)
//j.finish()
}
return interp.NewExitStatus(exit)
})(s.runner)
err = s.runner.Run(context.TODO(), stmt)
if err != nil {
return bg, strms.Stdout, strms.Stderr, err
}
}
return bg, strms.Stdout, strms.Stderr, nil
}
func splitInput(input string) ([]string, string) {
// end my suffering
// TODO: refactor this garbage
quoted := false
cmdArgs := []string{}
sb := &strings.Builder{}
cmdstr := &strings.Builder{}
for _, r := range input {
if r == '"' {
// start quoted input
// this determines if other runes are replaced
quoted = !quoted
// dont add back quotes
//sb.WriteRune(r)
} else if !quoted && r == '~' {
// if not in quotes and ~ is found then make it $HOME
sb.WriteString(os.Getenv("HOME"))
} else if !quoted && r == ' ' {
// if not quoted and there's a space then add to cmdargs
cmdArgs = append(cmdArgs, sb.String())
sb.Reset()
} else {
sb.WriteRune(r)
}
cmdstr.WriteRune(r)
}
if sb.Len() > 0 {
cmdArgs = append(cmdArgs, sb.String())
}
return cmdArgs, cmdstr.String()
}

View File

@ -1,147 +0,0 @@
// multi threading library
// Yarn is a simple multithreading library. Threads are individual Lua states,
// so they do NOT share the same environment as the code that runs the thread.
// Bait and Commanders are shared though, so you *can* throw hooks from 1 thread to another.
/*
Example:
```lua
local yarn = require 'yarn'
-- calling t will run the yarn thread.
local t = yarn.thread(print)
t 'printing from another lua state!'
```
*/
package yarn
import (
"fmt"
"hilbish/util"
"os"
"github.com/arnodel/golua/lib/packagelib"
rt "github.com/arnodel/golua/runtime"
)
var yarnMetaKey = rt.StringValue("hshyarn")
var globalSpool *Yarn
type Yarn struct {
initializer func(*rt.Runtime)
Loader packagelib.Loader
}
// #type
type Thread struct {
rtm *rt.Runtime
f rt.Callable
}
func New(init func(*rt.Runtime)) *Yarn {
yrn := &Yarn{
initializer: init,
}
yrn.Loader = packagelib.Loader{
Load: yrn.loaderFunc,
Name: "yarn",
}
globalSpool = yrn
return yrn
}
func (y *Yarn) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
yarnMeta := rt.NewTable()
yarnMeta.Set(rt.StringValue("__call"), rt.FunctionValue(rt.NewGoFunction(yarnrun, "__call", 1, true)))
rtm.SetRegistry(yarnMetaKey, rt.TableValue(yarnMeta))
exports := map[string]util.LuaExport{
"thread": {
Function: yarnthread,
ArgNum: 1,
Variadic: false,
},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
return rt.TableValue(mod), nil
}
func (y *Yarn) init(th *Thread) {
y.initializer(th.rtm)
}
// thread(fun) -> @Thread
// Creates a new, fresh Yarn thread.
// `fun` is the function that will run in the thread.
func yarnthread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
fun, err := c.CallableArg(0)
if err != nil {
return nil, err
}
yrn := &Thread{
rtm: rt.New(os.Stdout),
f: fun,
}
globalSpool.init(yrn)
return c.PushingNext(t.Runtime, rt.UserDataValue(yarnUserData(t.Runtime, yrn))), nil
}
func yarnrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
yrn, err := yarnArg(c, 0)
if err != nil {
return nil, err
}
yrn.Run(c.Etc())
return c.Next(), nil
}
func (y *Thread) Run(args []rt.Value) {
go func() {
term := rt.NewTerminationWith(y.rtm.MainThread().CurrentCont(), 0, true)
err := rt.Call(y.rtm.MainThread(), rt.FunctionValue(y.f), args, term)
if err != nil {
panic(err)
}
}()
}
func yarnArg(c *rt.GoCont, arg int) (*Thread, error) {
j, ok := valueToYarn(c.Arg(arg))
if !ok {
return nil, fmt.Errorf("#%d must be a yarn thread", arg+1)
}
return j, nil
}
func valueToYarn(val rt.Value) (*Thread, bool) {
u, ok := val.TryUserData()
if !ok {
return nil, false
}
j, ok := u.Value().(*Thread)
return j, ok
}
func yarnUserData(rtm *rt.Runtime, t *Thread) *rt.UserData {
yarnMeta := rtm.Registry(yarnMetaKey)
return rt.NewUserData(t, yarnMeta.AsTable())
}

6
job.go
View File

@ -56,8 +56,8 @@ func (j *job) start() error {
}
j.setHandle(&cmd)
}
// bgProcAttr is defined in job_<os>.go, it holds a procattr struct
// in a simple explanation, it makes signals from hilbish (like sigint)
// bgProcAttr is defined in execfile_<os>.go, it holds a procattr struct
// in a simple explanation, it makes signals from hilbish (sigint)
// not go to it (child process)
j.handle.SysProcAttr = bgProcAttr
// reset output buffers
@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if !j.running {
err := j.start()
exit := util.HandleExecErr(err)
exit := handleExecErr(err)
j.exitCode = int(exit)
j.finish()
}

View File

@ -10,10 +10,6 @@ import (
"golang.org/x/sys/unix"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
func (j *job) foreground() error {
if jobs.foreground {
return errors.New("(another) job already foregrounded")

View File

@ -4,13 +4,8 @@ package main
import (
"errors"
"syscall"
)
var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
func (j *job) foreground() error {
return errors.New("not supported on windows")
}

102
lua.go
View File

@ -3,40 +3,68 @@ package main
import (
"fmt"
"os"
"path/filepath"
"hilbish/util"
"hilbish/golibs/bait"
"hilbish/golibs/commander"
"hilbish/golibs/fs"
"hilbish/golibs/snail"
"hilbish/golibs/terminal"
"hilbish/golibs/yarn"
"hilbish/util"
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib"
"github.com/arnodel/golua/lib/debuglib"
rt "github.com/arnodel/golua/runtime"
)
var minimalconf = `hilbish.prompt '& '`
func luaInit() {
l = rt.New(os.Stdout)
l.PushContext(rt.RuntimeContextDef{
MessageHandler: debuglib.Traceback,
})
lib.LoadAll(l)
setupSinkType(l)
loadLibs(l)
lib.LoadLibs(l, hilbishLoader)
// yes this is stupid, i know
util.DoString(l, "hilbish = require 'hilbish'")
yarnPool := yarn.New(yarnloadLibs)
lib.LoadLibs(l, yarnPool.Loader)
// Add fs and terminal module module to Lua
f := fs.New(runner)
lib.LoadLibs(l, f.Loader)
lib.LoadLibs(l, terminal.Loader)
cmds = commander.New(l)
lib.LoadLibs(l, cmds.Loader)
hooks = bait.New(l)
hooks.SetRecoverer(func(event string, handler *bait.Listener, err interface{}) {
fmt.Println("Error in `error` hook handler:", err)
hooks.Off(event, handler)
})
lib.LoadLibs(l, hooks.Loader)
// Add Ctrl-C handler
hooks.On("signal.sigint", func(...interface{}) {
if !interactive {
os.Exit(0)
}
})
lr.rl.RawInputCallback = func(r []rune) {
hooks.Emit("hilbish.rawInput", string(r))
}
// Add more paths that Lua can require from
_, err := util.DoString(l, "package.path = package.path .. "+requirePaths)
_, err := util.DoString(l, "package.path = package.path .. " + requirePaths)
if err != nil {
fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.")
}
err1 := util.DoFile(l, "nature/init.lua")
if err1 != nil {
err2 := util.DoFile(l, filepath.Join(dataDir, "nature", "init.lua"))
err2 := util.DoFile(l, preloadPath)
if err2 != nil {
fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.")
fmt.Fprintln(os.Stderr, "local error:", err1)
@ -45,60 +73,6 @@ func luaInit() {
}
}
func loadLibs(r *rt.Runtime) {
r.PushContext(rt.RuntimeContextDef{
MessageHandler: debuglib.Traceback,
})
lib.LoadAll(r)
lib.LoadLibs(r, hilbishLoader)
// yes this is stupid, i know
util.DoString(r, "hilbish = require 'hilbish'")
hooks = bait.New(r)
hooks.SetRecoverer(func(event string, handler *bait.Listener, err interface{}) {
fmt.Println("Error in `error` hook handler:", err)
hooks.Off(event, handler)
})
lib.LoadLibs(r, hooks.Loader)
// Add Ctrl-C handler
hooks.On("signal.sigint", func(...interface{}) rt.Value {
if !interactive {
os.Exit(0)
}
return rt.NilValue
})
lr.rl.RawInputCallback = func(rn []rune) {
hooks.Emit("hilbish.rawInput", string(rn))
}
lib.LoadLibs(r, fs.Loader)
lib.LoadLibs(r, terminal.Loader)
lib.LoadLibs(r, snail.Loader)
cmds = commander.New(r)
lib.LoadLibs(r, cmds.Loader)
lib.LoadLibs(l, lr.rl.Loader)
}
func yarnloadLibs(r *rt.Runtime) {
r.PushContext(rt.RuntimeContextDef{
MessageHandler: debuglib.Traceback,
})
lib.LoadAll(r)
lib.LoadLibs(r, hilbishLoader)
lib.LoadLibs(r, hooks.Loader)
lib.LoadLibs(r, fs.Loader)
lib.LoadLibs(r, terminal.Loader)
lib.LoadLibs(r, snail.Loader)
lib.LoadLibs(r, cmds.Loader)
lib.LoadLibs(l, lr.rl.Loader)
}
func runConfig(confpath string) {
if !interactive {
return

55
main.go
View File

@ -21,6 +21,7 @@ import (
"github.com/pborman/getopt"
"github.com/maxlandon/readline"
"golang.org/x/term"
"mvdan.cc/sh/v3/interp"
)
var (
@ -37,27 +38,16 @@ var (
cmds *commander.Commander
defaultConfPath string
defaultHistPath string
runner *interp.Runner
)
func main() {
if runtime.GOOS == "linux" {
// dataDir should only be empty on linux to allow XDG_DATA_DIRS searching.
// but since it might be set on some distros (nixos) we should still check if its really is empty.
if dataDir == "" {
searchableDirs := getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")
dataDir = "."
for _, path := range strings.Split(searchableDirs, ":") {
_, err := os.Stat(filepath.Join(path, "hilbish", ".hilbishrc.lua"))
if err == nil {
dataDir = filepath.Join(path, "hilbish")
break
}
}
}
}
runner, _ = interp.New()
curuser, _ = user.Current()
homedir := curuser.HomeDir
confDir, _ = os.UserConfigDir()
preloadPath = strings.Replace(preloadPath, "~", homedir, 1)
sampleConfPath = strings.Replace(sampleConfPath, "~", homedir, 1)
// i honestly dont know what directories to use for this
switch runtime.GOOS {
@ -151,11 +141,10 @@ func main() {
confpath := ".hilbishrc.lua"
if err != nil {
// If it wasnt found, go to the real sample conf
sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua")
_, err = os.ReadFile(sampleConfigPath)
confpath = sampleConfigPath
_, err = os.ReadFile(sampleConfPath)
confpath = sampleConfPath
if err != nil {
fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath)
fmt.Println("could not find .hilbishrc.lua or", sampleConfPath)
return
}
}
@ -234,9 +223,8 @@ input:
}
if strings.HasSuffix(input, "\\") {
print("\n")
for {
input, err = continuePrompt(strings.TrimSuffix(input, "\\") + "\n", false)
input, err = continuePrompt(input)
if err != nil {
running = true
lr.SetPrompt(fmtPrompt(prompt))
@ -260,24 +248,16 @@ input:
exit(0)
}
func continuePrompt(prev string, newline bool) (string, error) {
func continuePrompt(prev string) (string, error) {
hooks.Emit("multiline", nil)
lr.SetPrompt(multilinePrompt)
cont, err := lr.Read()
if err != nil {
return "", err
}
cont = strings.TrimSpace(cont)
if newline {
cont = "\n" + cont
}
if strings.HasSuffix(cont, "\\") {
cont = strings.TrimSuffix(cont, "\\") + "\n"
}
return prev + cont, nil
return prev + strings.TrimSuffix(cont, "\n"), nil
}
// This semi cursed function formats our prompt (obviously)
@ -324,6 +304,15 @@ func removeDupes(slice []string) []string {
return newSlice
}
func contains(s []string, e string) bool {
for _, a := range s {
if strings.ToLower(a) == strings.ToLower(e) {
return true
}
}
return false
}
func exit(code int) {
jobs.stopAll()

View File

@ -1,61 +0,0 @@
-- @module hilbish.abbr
-- command line abbreviations
-- The abbr module manages Hilbish abbreviations. These are words that can be replaced
-- with longer command line strings when entered.
-- As an example, `git push` can be abbreviated to `gp`. When the user types
-- `gp` into the command line, after hitting space or enter, it will expand to `git push`.
-- Abbreviations can be used as an alternative to aliases. They are saved entirely in the history
-- Instead of the aliased form of the same command.
local bait = require 'bait'
local hilbish = require 'hilbish'
hilbish.abbr = {
all = {}
}
--- Adds an abbreviation. The `abbr` is the abbreviation itself,
--- while `expanded` is what the abbreviation should expand to.
--- It can be either a function or a string. If it is a function, it will expand to what
--- the function returns.
--- `opts` is a table that accepts 1 key: `anywhere`.
--- `opts.anywhere` defines whether the abbr expands anywhere in the command line or not,
--- whereas the default behavior is only at the beginning of the line
-- @param abbr string
-- @param expanded|function string
-- @param opts table
function hilbish.abbr.add(abbr, expanded, opts)
print(abbr, expanded, opts)
opts = opts or {}
opts.abbr = abbr
opts.expand = expanded
hilbish.abbr.all[abbr] = opts
end
--- Removes the named `abbr`.
-- @param abbr string
function hilbish.abbr.remove(abbr)
hilbish.abbr.all[abbr] = nil
end
bait.catch('hilbish.rawInput', function(c)
-- 0x0d == enter
if c == ' ' or c == string.char(0x0d) then
-- check if the last "word" was a valid abbreviation
local line = hilbish.editor.getLine()
local lineSplits = string.split(line, ' ')
local thisAbbr = hilbish.abbr.all[lineSplits[#lineSplits]]
if thisAbbr and (#lineSplits == 1 or thisAbbr.anywhere == true) then
hilbish.editor.deleteByAmount(-lineSplits[#lineSplits]:len())
if type(thisAbbr.expand) == 'string' then
hilbish.editor.insert(thisAbbr.expand)
elseif type(thisAbbr.expand) == 'function' then
local expandRet = thisAbbr.expand()
if type(expandRet) ~= 'string' then
print(string.format('abbr %s has an expand function that did not return a string. instead it returned: %s', thisAbbr.abbr, expandRet))
return
end
hilbish.editor.insert(expandRet)
end
end
end
end)

View File

@ -3,9 +3,8 @@ local commander = require 'commander'
local fs = require 'fs'
local dirs = require 'nature.dirs'
dirs.old = hilbish.cwd()
commander.register('cd', function (args, sinks)
local oldPath = hilbish.cwd()
if #args > 1 then
sinks.out:writeln("cd: too many arguments")
return 1
@ -17,12 +16,13 @@ commander.register('cd', function (args, sinks)
sinks.out:writeln(path)
end
local absPath = fs.abs(path)
dirs.setOld(hilbish.cwd())
dirs.push(path)
local ok, err = pcall(function() fs.cd(path) end)
if not ok then
sinks.out:writeln(err)
return 1
end
bait.throw('cd', path, oldPath)
bait.throw('cd', path)
end)

View File

@ -1,13 +1,10 @@
-- @module dirs
-- internal directory management
-- The dirs module defines a small set of functions to store and manage
-- directories.
local bait = require 'bait'
local fs = require 'fs'
local dirs = {}
--- Last (current working) directory. Separate from recentDirs mainly for easier use.
--- Last (current working) directory. Separate from recentDirs mainly for
--- easier use.
dirs.old = ''
--- Table of recent directories. For use, look at public functions.
dirs.recentDirs = {}
@ -38,21 +35,19 @@ function dirRecents(num, remove)
end
--- Look at `num` amount of recent directories, starting from the latest.
--- This returns a table of recent directories, up to the `num` amount.
-- @param num? number
function dirs.peak(num)
return dirRecents(num)
end
--- Add `dir` to the recent directories list.
--- @param dir string
function dirs.push(dir)
--- Add `d` to the recent directories list.
function dirs.push(d)
dirs.recentDirs[dirs.recentSize + 1] = nil
if dirs.recentDirs[#dirs.recentDirs - 1] ~= dir then
local ok, dir = pcall(fs.abs, dir)
assert(ok, 'could not turn "' .. dir .. '"into an absolute path')
if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then
ok, d = pcall(fs.abs, d)
assert(ok, 'could not turn "' .. d .. '"into an absolute path')
table.insert(dirs.recentDirs, 1, dir)
table.insert(dirs.recentDirs, 1, d)
end
end
@ -78,9 +73,4 @@ function dirs.setOld(d)
dirs.old = d
end
bait.catch('hilbish.cd', function(path, oldPath)
dirs.setOld(oldPath)
dirs.push(path)
end)
return dirs

View File

@ -1,25 +1,13 @@
-- @module doc
-- command-line doc rendering
-- The doc module contains a small set of functions
-- used by the Greenhouse pager to render parts of the documentation pages.
-- This is only documented for the sake of it. It's only intended use
-- is by the Greenhouse pager.
local lunacolors = require 'lunacolors'
local doc = {}
local M = {}
--- Performs basic Lua code highlighting.
--- @param text string Code/text to do highlighting on.
function doc.highlight(text)
function M.highlight(text)
return text:gsub('\'.-\'', lunacolors.yellow)
--:gsub('%-%- .-', lunacolors.black)
end
--- Assembles and renders a code block. This returns
--- the supplied text based on the number of command line columns,
--- and styles it to resemble a code block.
--- @param text string
function doc.renderCodeBlock(text)
function M.renderCodeBlock(text)
local longest = 0
local lines = string.split(text:gsub('\t', ' '), '\n')
@ -29,18 +17,14 @@ function doc.renderCodeBlock(text)
end
for i, line in ipairs(lines) do
lines[i] = lunacolors.format('{greyBg}' .. ' ' .. doc.highlight(line:sub(0, longest))
lines[i] = lunacolors.format('{greyBg}' .. ' ' .. M.highlight(line:sub(0, longest))
.. string.rep(' ', longest - line:len()) .. ' ')
end
return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
end
--- Renders an info block. An info block is a block of text with
--- an icon and styled text block.
--- @param type string Type of info block. The only one specially styled is the `warning`.
--- @param text string
function doc.renderInfoBlock(type, text)
function M.renderInfoBlock(type, text)
local longest = 0
local lines = string.split(text:gsub('\t', ' '), '\n')
@ -50,7 +34,7 @@ function doc.renderInfoBlock(type, text)
end
for i, line in ipairs(lines) do
lines[i] = ' ' .. doc.highlight(line:sub(0, longest))
lines[i] = ' ' .. M.highlight(line:sub(0, longest))
.. string.rep(' ', longest - line:len()) .. ' '
end
@ -60,4 +44,4 @@ function doc.renderInfoBlock(type, text)
end
return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
end
return doc
return M

View File

@ -1,28 +0,0 @@
local readline = require 'readline'
local editor = readline.new()
local editorMt = {}
hilbish.editor = {}
local function contains(search, needle)
for _, p in ipairs(search) do
if p == needle then
return true
end
end
return false
end
function editorMt.__index(_, key)
if contains({'deleteByAmount', 'getVimRegister', 'getLine', 'insert', 'readChar', 'setVimRegister'}, key) then
--editor:log 'The calling method of this function has changed. Please use the colon to call this hilbish.editor function.'
end
return function(...)
return editor[key](editor, ...)
end
end
setmetatable(hilbish.editor, editorMt)

View File

@ -1,10 +0,0 @@
env = {}
setmetatable(env, {
__index = function(_, k)
return os.getenv(k)
end,
__newindex = function(_, k, v)
os.setenv(k, tostring(v))
end
})

View File

@ -1,5 +1,4 @@
-- @module greenhouse
-- Greenhouse is a simple text scrolling handler (pager) for terminal programs.
-- Greenhouse is a simple text scrolling handler for terminal programs.
-- The idea is that it can be set a region to do its scrolling and paging
-- job and then the user can draw whatever outside it.
-- This reduces code duplication for the message viewer

View File

@ -1,4 +1,3 @@
-- @module greenhouse.page
local Object = require 'nature.object'
local Page = Object:extend()
@ -11,7 +10,6 @@ function Page:new(title, text)
self.children = {}
end
function Page:setText(text)
self.lines = string.split(text, '\n')
end

View File

@ -1,79 +0,0 @@
-- @module hilbish
local bait = require 'bait'
local snail = require 'snail'
hilbish.snail = snail.new()
hilbish.snail:run 'true' -- to "initialize" snail
bait.catch('hilbish.cd', function(path)
hilbish.snail:dir(path)
end)
--- Runs `cmd` in Hilbish's shell script interpreter.
--- The `streams` parameter specifies the output and input streams the command should use.
--- For example, to write command output to a sink.
--- As a table, the caller can directly specify the standard output, error, and input
--- streams of the command with the table keys `out`, `err`, and `input` respectively.
--- As a boolean, it specifies whether the command should use standard output or return its output streams.
--- #example
--- -- This code is the same as `ls -l | wc -l`
--- local fs = require 'fs'
--- local pr, pw = fs.pipe()
--- hilbish.run('ls -l', {
--- stdout = pw,
--- stderr = pw,
--- })
--- pw:close()
--- hilbish.run('wc -l', {
--- stdin = pr
--- })
--- #example
-- @param cmd string
-- @param streams table|boolean
-- @returns number, string, string
function hilbish.run(cmd, streams)
local sinks = {}
if type(streams) == 'boolean' then
if not streams then
sinks = {
out = hilbish.sink.new(),
err = hilbish.sink.new(),
input = io.stdin
}
end
elseif type(streams) == 'table' then
sinks = streams
end
local out = hilbish.snail:run(cmd, {sinks = sinks})
local returns = {out.exitCode}
if type(streams) == 'boolean' and not streams then
table.insert(returns, sinks.out:readAll())
table.insert(returns, sinks.err:readAll())
end
return table.unpack(returns)
end
--- Sets the execution/runner mode for interactive Hilbish.
--- **NOTE: This function is deprecated and will be removed in 3.0**
--- Use `hilbish.runner.setCurrent` instead.
--- 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.
--- Read [about runner mode](../features/runner-mode) for more information.
-- @param mode string|function
function hilbish.runnerMode(mode)
if type(mode) == 'string' then
hilbish.runner.setCurrent(mode)
elseif type(mode) == 'function' then
hilbish.runner.set('_', {
run = mode
})
hilbish.runner.setCurrent '_'
else
error('expected runner mode type to be either string or function, got', type(mode))
end
end

View File

@ -1,14 +1,3 @@
-- @module hilbish.messages
-- simplistic message passing
-- The messages interface defines a way for Hilbish-integrated commands,
-- user config and other tasks to send notifications to alert the user.z
-- The `hilbish.message` type is a table with the following keys:
-- `title` (string): A title for the message notification.
-- `text` (string): The contents of the message.
-- `channel` (string): States the origin of the message, `hilbish.*` is reserved for Hilbish tasks.
-- `summary` (string): A short summary of the `text`.
-- `icon` (string): Unicode (preferably standard emoji) icon for the message notification
-- `read` (boolean): Whether the full message has been read or not.
local bait = require 'bait'
local commander = require 'commander'
local lunacolors = require 'lunacolors'
@ -55,8 +44,6 @@ function hilbish.messages.send(message)
bait.throw('hilbish.notification', message)
end
--- Marks a message at `idx` as read.
--- @param idx number
function hilbish.messages.read(idx)
local msg = M._messages[idx]
if msg then
@ -65,20 +52,16 @@ function hilbish.messages.read(idx)
end
end
--- Marks all messages as read.
function hilbish.messages.readAll()
function hilbish.messages.readAll(idx)
for _, msg in ipairs(hilbish.messages.all()) do
hilbish.messages.read(msg.index)
end
end
--- Returns the amount of unread messages.
function hilbish.messages.unreadCount()
return unread
end
--- Deletes the message at `idx`.
--- @param idx number
function hilbish.messages.delete(idx)
local msg = M._messages[idx]
if not msg then
@ -88,14 +71,12 @@ function hilbish.messages.delete(idx)
M._messages[idx] = nil
end
--- Deletes all messages.
function hilbish.messages.clear()
for _, msg in ipairs(hilbish.messages.all()) do
hilbish.messages.delete(msg.index)
end
end
--- Returns all messages.
function hilbish.messages.all()
return M._messages
end

View File

@ -18,18 +18,12 @@ table.insert(package.searchers, function(module)
return function() return hilbish.module.load(path) end, path
end)
require 'nature.hilbish'
require 'nature.processors'
require 'nature.commands'
require 'nature.completions'
require 'nature.opts'
require 'nature.vim'
require 'nature.runner'
require 'nature.hummingbird'
require 'nature.env'
require 'nature.abbr'
require 'nature.editor'
local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then
@ -38,6 +32,36 @@ else
os.setenv('SHLVL', '0')
end
do
local virt_G = { }
setmetatable(_G, {
__index = function (_, key)
local got_virt = virt_G[key]
if got_virt ~= nil then
return got_virt
end
if type(key) == 'string' then
virt_G[key] = os.getenv(key)
end
return virt_G[key]
end,
__newindex = function (_, key, value)
if type(value) == 'string' then
os.setenv(key, value)
virt_G[key] = value
else
if type(virt_G[key]) == 'string' then
os.setenv(key, '')
end
virt_G[key] = value
end
end,
})
end
do
local startSearchPath = hilbish.userDir.data .. '/hilbish/start/?/init.lua;'
.. hilbish.userDir.data .. '/hilbish/start/?.lua'

View File

@ -1,27 +1,18 @@
local fs = require 'fs'
hilbish.processors.add {
name = 'hilbish.autocd',
func = function(path)
if hilbish.opts.autocd then
local ok, stat = pcall(fs.stat, path)
if ok and stat.isDir then
local oldPath = hilbish.cwd()
local oldShRunner = hilbish.runner.sh
function hilbish.runner.sh(input)
local res = oldShRunner(input)
local absPath = fs.abs(path)
fs.cd(path)
bait.throw('cd', path, oldPath)
bait.throw('hilbish.cd', absPath, oldPath)
end
return {
continue = not ok
}
else
return {
continue = true
}
if res.exit ~= 0 and hilbish.opts.autocd then
local ok, stat = pcall(fs.stat, res.input)
if ok and stat.isDir then
-- discard here to not append the cd, which will be in history
local _, exitCode, err = hilbish.runner.sh('cd ' .. res.input)
res.exitCode = exitCode
res.err = err
end
end
}
return res
end

View File

@ -2,7 +2,7 @@ hilbish.opts = {}
local function setupOpt(name, default)
hilbish.opts[name] = default
local ok, err = pcall(require, 'nature.opts.' .. name)
pcall(require, 'nature.opts.' .. name)
end
local defaultOpts = {
@ -15,8 +15,7 @@ The nice lil shell for {blue}Lua{reset} fanatics!
fuzzy = false,
notifyJobFinish = true,
crimmas = true,
tips = true,
processorSkipList = {}
tips = true
}
for optsName, default in pairs(defaultOpts) do

View File

@ -2,7 +2,9 @@ local bait = require 'bait'
local lunacolors = require 'lunacolors'
hilbish.motd = [[
{magenta}Hilbish{reset} blooms in the {blue}midnight.{reset}
Wait ... {magenta}2.3{reset} is basically the same as {red}2.2?{reset}
Erm.. {blue}Ctrl-C works for Commanders,{reset} {cyan}and the sh runner has some fixes.{reset}
Just trust me bro, this is an imporant bug fix release. {red}- 🌺 sammyette{reset}
]]
bait.catch('hilbish.init', function()

View File

@ -1,57 +0,0 @@
-- @module hilbish.processors
hilbish.processors = {
list = {},
sorted = {}
}
function hilbish.processors.add(processor)
if not processor.name then
error 'processor is missing name'
end
if not processor.func then
error 'processor is missing function'
end
table.insert(hilbish.processors.list, processor)
table.sort(hilbish.processors.list, function(a, b) return a.priority < b.priority end)
end
local function contains(search, needle)
for _, p in ipairs(search) do
if p == needle then
return true
end
end
return false
end
--- Run all command processors, in order by priority.
--- It returns the processed command (which may be the same as the passed command)
--- and a boolean which states whether to proceed with command execution.
function hilbish.processors.execute(command, opts)
opts = opts or {}
opts.skip = opts.skip or {}
local continue = true
local history
for _, processor in ipairs(hilbish.processors.list) do
if not contains(opts.skip, processor.name) then
local processed = processor.func(command)
if processed.history ~= nil then history = processed.history end
if processed.command then command = processed.command end
if not processed.continue then
continue = false
break
end
end
end
return {
command = command,
continue = continue,
history = history
}
end

View File

@ -1,5 +1,4 @@
-- @module hilbish.runner
local snail = require 'snail'
--- hilbish.runner
local currentRunner = 'hybrid'
local runners = {}
@ -7,7 +6,7 @@ local runners = {}
hilbish = hilbish
--- Get a runner by name.
--- @param name string Name of the runner to retrieve.
--- @param name string
--- @return table
function hilbish.runner.get(name)
local r = runners[name]
@ -19,10 +18,10 @@ function hilbish.runner.get(name)
return r
end
--- Adds a runner to the table of available runners.
--- If runner is a table, it must have the run function in it.
--- @param name string Name of the runner
--- @param runner function|table
--- Adds a runner to the table of available runners. If runner is a table,
--- it must have the run function in it.
--- @param name string
--- @param runner function | table
function hilbish.runner.add(name, runner)
if type(name) ~= 'string' then
error 'expected runner name to be a table'
@ -43,9 +42,7 @@ function hilbish.runner.add(name, runner)
hilbish.runner.set(name, runner)
end
--- *Sets* a runner by name. The difference between this function and
--- add, is set will *not* check if the named runner exists.
--- The runner table must have the run function in it.
--- Sets a runner by name. The runner table must have the run function in it.
--- @param name string
--- @param runner table
function hilbish.runner.set(name, runner)
@ -56,11 +53,11 @@ function hilbish.runner.set(name, runner)
runners[name] = runner
end
--- Executes `cmd` with a runner.
--- If `runnerName` is not specified, it uses the default Hilbish runner.
--- Executes cmd with a runner. If runnerName isn't passed, it uses
--- the user's current runner.
--- @param cmd string
--- @param runnerName string?
--- @return table
--- @return string, number, string
function hilbish.runner.exec(cmd, runnerName)
if not runnerName then runnerName = currentRunner end
@ -69,11 +66,13 @@ function hilbish.runner.exec(cmd, runnerName)
return r.run(cmd)
end
--- Sets Hilbish's runner mode by name.
--- Sets the current interactive/command line runner mode.
--- @param name string
function hilbish.runner.setCurrent(name)
hilbish.runner.get(name) -- throws if it doesnt exist.
local r = hilbish.runner.get(name)
currentRunner = name
hilbish.runner.setMode(r.run)
end
--- Returns the current runner by name.
@ -82,91 +81,6 @@ function hilbish.runner.getCurrent()
return currentRunner
end
--- **NOTE: This function is deprecated and will be removed in 3.0**
--- Use `hilbish.runner.setCurrent` instead.
--- This is the same as the `hilbish.runnerMode` function.
--- It takes a callback, which will be used to execute all interactive input.
--- Or a string which names the runner mode to use.
-- @param mode string|function
function hilbish.runner.setMode(mode)
hilbish.runnerMode(mode)
end
local function finishExec(exitCode, input, priv)
hilbish.exitCode = exitCode
bait.throw('command.exit', exitCode, input, priv)
end
local function continuePrompt(prev, newline)
local multilinePrompt = hilbish.multiprompt()
-- the return of hilbish.read is nil when error or ctrl-d
local cont = hilbish.read(multilinePrompt)
if not cont then
return
end
if newline then
cont = '\n' .. cont
end
if cont:match '\\$' then
cont = cont:gsub('\\$', '') .. '\n'
end
return prev .. cont
end
--- Runs `input` with the currently set Hilbish runner.
--- This method is how Hilbish executes commands.
--- `priv` is an optional boolean used to state if the input should be saved to history.
-- @param input string
-- @param priv bool
function hilbish.runner.run(input, priv)
bait.throw('command.preprocess', input)
local processed = hilbish.processors.execute(input, {
skip = hilbish.opts.processorSkipList
})
priv = processed.history ~= nil and (not processed.history) or priv
if not processed.continue then
finishExec(0, '', true)
return
end
local command = hilbish.aliases.resolve(processed.command)
bait.throw('command.preexec', processed.command, command)
::rerun::
local runner = hilbish.runner.get(currentRunner)
local ok, out = pcall(runner.run, processed.command)
if not ok then
io.stderr:write(out .. '\n')
finishExec(124, out.input, priv)
return
end
if out.continue then
local contInput = continuePrompt(processed.command, out.newline)
if contInput then
processed.command = contInput
goto rerun
end
end
if out.err then
local fields = string.split(out.err, ': ')
if fields[2] == 'not-found' or fields[2] == 'not-executable' then
bait.throw('command.' .. fields[2], fields[1])
else
io.stderr:write(out.err .. '\n')
end
end
finishExec(out.exitCode, out.input, priv)
end
function hilbish.runner.sh(input)
return hilbish.snail:run(input)
end
hilbish.runner.add('hybrid', function(input)
local cmdStr = hilbish.aliases.resolve(input)
@ -193,5 +107,7 @@ hilbish.runner.add('lua', function(input)
return hilbish.runner.lua(cmdStr)
end)
hilbish.runner.add('sh', hilbish.runner.sh)
hilbish.runner.setCurrent 'hybrid'
hilbish.runner.add('sh', function(input)
return hilbish.runner.sh(input)
end)

View File

@ -54,14 +54,14 @@ var (
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 ^["
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})
seqPageUp = string([]byte{27, 91, 53, 126})
seqPageDown = string([]byte{27, 91, 54, 126})
seqPageDown = string([]byte{27, 91, 54, 126})
)
const (
@ -76,7 +76,7 @@ const (
seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left
seqGetCursorPos = "\x1b6n" // response: "\x1b{Line};{Column}R"
seqHideCursor = "\x1b[?25l"
seqHideCursor = "\x1b[?25l"
seqUnhideCursor = "\x1b[?25h"
seqCtrlLeftArrow = "\x1b[1;5D"
@ -143,94 +143,55 @@ const (
// TODO: return whether its actually a sequence or not
// remedies the edge case of someone literally typing Ctrl-A for example.
func (rl *Readline) ReadChar() string {
func (rl *Instance) ReadChar() string {
b := make([]byte, 1024)
i, _ := os.Stdin.Read(b)
r := []rune(string(b))
s := string(r[:i])
switch b[0] {
case charCtrlA:
return "Ctrl-A"
case charCtrlB:
return "Ctrl-B"
case charCtrlC:
return "Ctrl-C"
case charEOF:
return "Ctrl-D"
case charCtrlE:
return "Ctrl-E"
case charCtrlF:
return "Ctrl-F"
case charCtrlG:
return "Ctrl-G"
case charBackspace, charBackspace2:
return "Backspace"
case charTab:
return "Tab"
case charCtrlK:
return "Ctrl-K"
case charCtrlL:
return "Ctrl-L"
case charCtrlN:
return "Ctrl-N"
case charCtrlO:
return "Ctrl-O"
case charCtrlP:
return "Ctrl-P"
case charCtrlQ:
return "Ctrl-Q"
case charCtrlR:
return "Ctrl-R"
case charCtrlS:
return "Ctrl-S"
case charCtrlT:
return "Ctrl-T"
case charCtrlU:
return "Ctrl-U"
case charCtrlV:
return "Ctrl-V"
case charCtrlW:
return "Ctrl-W"
case charCtrlX:
return "Ctrl-X"
case charCtrlY:
return "Ctrl-Y"
case charCtrlZ:
return "Ctrl-Z"
case '\r':
fallthrough
case '\n':
return "Enter"
case charEscape:
switch s {
case string(charEscape):
return "Escape"
case seqUp:
return "Up"
case seqDown:
return "Down"
case seqBackwards:
return "Left"
case seqForwards:
return "Right"
case seqCtrlLeftArrow:
return "Ctrl-Left"
case seqCtrlRightArrow:
return "Ctrl-Right"
case seqCtrlDelete, seqCtrlDelete2:
return "Ctrl-Delete"
case seqHome, seqHomeSc:
return "Home"
case seqEnd, seqEndSc:
return "End"
case seqDelete, seqDelete2:
return "Delete"
case seqPageUp:
return "Page-Up"
case seqPageDown:
return "Page-Down"
}
case charCtrlA: return "Ctrl-A"
case charCtrlB: return "Ctrl-B"
case charCtrlC: return "Ctrl-C"
case charEOF: return "Ctrl-D"
case charCtrlE: return "Ctrl-E"
case charCtrlF: return "Ctrl-F"
case charCtrlG: return "Ctrl-G"
case charBackspace, charBackspace2: return "Backspace"
case charTab: return "Tab"
case charCtrlK: return "Ctrl-K"
case charCtrlL: return "Ctrl-L"
case charCtrlN: return "Ctrl-N"
case charCtrlO: return "Ctrl-O"
case charCtrlP: return "Ctrl-P"
case charCtrlQ: return "Ctrl-Q"
case charCtrlR: return "Ctrl-R"
case charCtrlS: return "Ctrl-S"
case charCtrlT: return "Ctrl-T"
case charCtrlU: return "Ctrl-U"
case charCtrlV: return "Ctrl-V"
case charCtrlW: return "Ctrl-W"
case charCtrlX: return "Ctrl-X"
case charCtrlY: return "Ctrl-Y"
case charCtrlZ: return "Ctrl-Z"
case '\r': fallthrough
case '\n': return "Enter"
case charEscape:
switch s {
case string(charEscape): return "Escape"
case seqUp: return "Up"
case seqDown: return "Down"
case seqBackwards: return "Left"
case seqForwards: return "Right"
case seqCtrlLeftArrow: return "Ctrl-Left"
case seqCtrlRightArrow: return "Ctrl-Right"
case seqCtrlDelete, seqCtrlDelete2: return "Ctrl-Delete"
case seqHome, seqHomeSc: return "Home"
case seqEnd, seqEndSc: return "End"
case seqDelete, seqDelete2: return "Delete"
case seqPageUp: return "Page-Up"
case seqPageDown: return "Page-Down"
}
}
return s

View File

@ -4,13 +4,12 @@ import (
"fmt"
"strconv"
"strings"
"github.com/rivo/uniseg"
)
// initGrid - Grid display details. Called each time we want to be sure to have
// a working completion group either immediately, or later on. Generally defered.
func (g *CompletionGroup) initGrid(rl *Readline) {
func (g *CompletionGroup) initGrid(rl *Instance) {
// Compute size of each completion item box
tcMaxLength := 1
@ -45,7 +44,7 @@ func (g *CompletionGroup) initGrid(rl *Readline) {
}
// moveTabGridHighlight - Moves the highlighting for currently selected completion item (grid display)
func (g *CompletionGroup) moveTabGridHighlight(rl *Readline, x, y int) (done bool, next bool) {
func (g *CompletionGroup) moveTabGridHighlight(rl *Instance, x, y int) (done bool, next bool) {
g.tcPosX += x
g.tcPosY += y
@ -97,7 +96,7 @@ func (g *CompletionGroup) moveTabGridHighlight(rl *Readline, x, y int) (done boo
}
// writeGrid - A grid completion string
func (g *CompletionGroup) writeGrid(rl *Readline) (comp string) {
func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) {
// If group title, print it and adjust offset.
if g.Name != "" {
@ -128,9 +127,9 @@ func (g *CompletionGroup) writeGrid(rl *Readline) (comp string) {
sugg := g.Suggestions[i]
if len(sugg) > GetTermWidth() {
sugg = sugg[:GetTermWidth()-4] + "..."
sugg = sugg[:GetTermWidth() - 4] + "..."
}
formatStr := "%-" + cellWidth + "s%s "
formatStr := "%-"+cellWidth+"s%s "
if g.tcMaxX == 1 {
formatStr = "%s%s"
}

View File

@ -14,7 +14,6 @@ type CompletionGroup struct {
Suggestions []string
Aliases map[string]string // A candidate has an alternative name (ex: --long, -l option flags)
Descriptions map[string]string // Items descriptions
ItemDisplays map[string]string // What to display the item as (can be used for styling items)
DisplayType TabDisplayType // Map, list or normal
MaxLength int // Each group can be limited in the number of comps offered
@ -49,7 +48,7 @@ type CompletionGroup struct {
}
// init - The completion group computes and sets all its values, and is then ready to work.
func (g *CompletionGroup) init(rl *Readline) {
func (g *CompletionGroup) init(rl *Instance) {
// Details common to all displays
g.checkCycle(rl) // Based on the number of groups given to the shell, allows cycling or not
@ -70,7 +69,7 @@ func (g *CompletionGroup) init(rl *Readline) {
// updateTabFind - When searching through all completion groups (whether it be command history or not),
// we ask each of them to filter its own items and return the results to the shell for aggregating them.
// The rx parameter is passed, as the shell already checked that the search pattern is valid.
func (g *CompletionGroup) updateTabFind(rl *Readline) {
func (g *CompletionGroup) updateTabFind(rl *Instance) {
suggs := rl.Searcher(rl.search, g.Suggestions)
// We perform filter right here, so we create a new completion group, and populate it with our results.
@ -97,7 +96,7 @@ func (g *CompletionGroup) updateTabFind(rl *Readline) {
}
// checkCycle - Based on the number of groups given to the shell, allows cycling or not
func (g *CompletionGroup) checkCycle(rl *Readline) {
func (g *CompletionGroup) checkCycle(rl *Instance) {
if len(rl.tcGroups) == 1 {
g.allowCycle = true
}
@ -108,7 +107,7 @@ func (g *CompletionGroup) checkCycle(rl *Readline) {
}
// checkMaxLength - Based on the number of groups given to the shell, check/set MaxLength defaults
func (g *CompletionGroup) checkMaxLength(rl *Readline) {
func (g *CompletionGroup) checkMaxLength(rl *Instance) {
// This means the user forgot to set it
if g.MaxLength == 0 {
@ -147,7 +146,7 @@ func checkNilItems(groups []*CompletionGroup) (checked []*CompletionGroup) {
// writeCompletion - This function produces a formatted string containing all appropriate items
// and according to display settings. This string is then appended to the main completion string.
func (g *CompletionGroup) writeCompletion(rl *Readline) (comp string) {
func (g *CompletionGroup) writeCompletion(rl *Instance) (comp string) {
// Avoids empty groups in suggestions
if len(g.Suggestions) == 0 {
@ -169,7 +168,7 @@ func (g *CompletionGroup) writeCompletion(rl *Readline) (comp string) {
// getCurrentCell - The completion groups computes the current cell value,
// depending on its display type and its different parameters
func (g *CompletionGroup) getCurrentCell(rl *Readline) string {
func (g *CompletionGroup) getCurrentCell(rl *Instance) string {
switch g.DisplayType {
case TabDisplayGrid:

View File

@ -8,7 +8,7 @@ import (
// initList - List display details. Because of the way alternative completions
// are handled, MaxLength cannot be set when there are alternative completions.
func (g *CompletionGroup) initList(rl *Readline) {
func (g *CompletionGroup) initList(rl *Instance) {
// We may only ever have two different
// columns: (suggestions, and alternatives)
@ -53,7 +53,7 @@ func (g *CompletionGroup) initList(rl *Readline) {
// moveTabListHighlight - Moves the highlighting for currently selected completion item (list display)
// We don't care about the x, because only can have 2 columns of selectable choices (--long and -s)
func (g *CompletionGroup) moveTabListHighlight(rl *Readline, x, y int) (done bool, next bool) {
func (g *CompletionGroup) moveTabListHighlight(rl *Instance, x, y int) (done bool, next bool) {
// We dont' pass to x, because not managed by callers
g.tcPosY += x
@ -153,7 +153,7 @@ func (g *CompletionGroup) moveTabListHighlight(rl *Readline, x, y int) (done boo
}
// writeList - A list completion string
func (g *CompletionGroup) writeList(rl *Readline) (comp string) {
func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
// Print group title and adjust offset if there is one.
if g.Name != "" {
@ -217,11 +217,6 @@ func (g *CompletionGroup) writeList(rl *Readline) (comp string) {
alt = strings.Repeat(" ", maxLengthAlt+1) // + 2 to keep account of spaces
}
styledSugg, ok := g.ItemDisplays[item]
if ok {
sugg = fmt.Sprintf("\r%s%-"+cellWidth+"s", highlight(y, 0), fmtEscape(styledSugg))
}
// Description
description := g.Descriptions[g.Suggestions[i]]
if len(description) > maxDescWidth {
@ -249,7 +244,7 @@ func (g *CompletionGroup) writeList(rl *Readline) (comp string) {
return
}
func (rl *Readline) getListPad() (pad int) {
func (rl *Instance) getListPad() (pad int) {
for _, group := range rl.tcGroups {
if group.DisplayType == TabDisplayList {
for i := range group.Suggestions {

View File

@ -7,7 +7,7 @@ import (
// initMap - Map display details. Called each time we want to be sure to have
// a working completion group either immediately, or later on. Generally defered.
func (g *CompletionGroup) initMap(rl *Readline) {
func (g *CompletionGroup) initMap(rl *Instance) {
// We make the map anyway, especially if we need to use it later
if g.Descriptions == nil {
@ -35,7 +35,7 @@ func (g *CompletionGroup) initMap(rl *Readline) {
}
// moveTabMapHighlight - Moves the highlighting for currently selected completion item (map display)
func (g *CompletionGroup) moveTabMapHighlight(rl *Readline, x, y int) (done bool, next bool) {
func (g *CompletionGroup) moveTabMapHighlight(rl *Instance, x, y int) (done bool, next bool) {
g.tcPosY += x
g.tcPosY += y
@ -72,7 +72,7 @@ func (g *CompletionGroup) moveTabMapHighlight(rl *Readline, x, y int) (done bool
}
// writeMap - A map or list completion string
func (g *CompletionGroup) writeMap(rl *Readline) (comp string) {
func (g *CompletionGroup) writeMap(rl *Instance) (comp string) {
if g.Name != "" {
// Print group title (changes with line returns depending on type)

View File

@ -0,0 +1,23 @@
package completers
import (
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// CompleteCommandArguments - Completes all values for arguments to a command.
// Arguments here are different from command options (--option).
// Many categories, from multiple sources in multiple contexts
func completeCommandArguments(cmd *flags.Command, arg string, lastWord string) (prefix string, completions []*readline.CompletionGroup) {
// the prefix is the last word, by default
prefix = lastWord
// SEE completeOptionArguments FOR A WAY TO ADD COMPLETIONS TO SPECIFIC ARGUMENTS ------------------------------
// found := argumentByName(cmd, arg)
// var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions
return
}

124
readline/completers/env.go Normal file
View File

@ -0,0 +1,124 @@
package completers
import (
"os"
"strings"
"github.com/maxlandon/readline"
)
// completeEnvironmentVariables - Returns all environment variables as suggestions
func completeEnvironmentVariables(lastWord string) (last string, completions []*readline.CompletionGroup) {
// Check if last input is made of several different variables
allVars := strings.Split(lastWord, "/")
lastVar := allVars[len(allVars)-1]
var evaluated = map[string]string{}
grp := &readline.CompletionGroup{
Name: "console OS environment",
MaxLength: 5, // Should be plenty enough
DisplayType: readline.TabDisplayGrid,
TrimSlash: true, // Some variables can be paths
}
for k, v := range clientEnv {
if strings.HasPrefix("$"+k, lastVar) {
grp.Suggestions = append(grp.Suggestions, "$"+k+"/")
evaluated[k] = v
}
}
completions = append(completions, grp)
return lastVar, completions
}
// clientEnv - Contains all OS environment variables, client-side.
// This is used for things like downloading/uploading files from localhost, etc.,
// therefore we need completion and parsing stuff, sometimes.
var clientEnv = map[string]string{}
// ParseEnvironmentVariables - Parses a line of input and replace detected environment variables with their values.
func ParseEnvironmentVariables(args []string) (processed []string, err error) {
for _, arg := range args {
// Anywhere a $ is assigned means there is an env variable
if strings.Contains(arg, "$") || strings.Contains(arg, "~") {
//Split in case env is embedded in path
envArgs := strings.Split(arg, "/")
// If its not a path
if len(envArgs) == 1 {
processed = append(processed, handleCuratedVar(arg))
}
// If len of the env var split is > 1, its a path
if len(envArgs) > 1 {
processed = append(processed, handleEmbeddedVar(arg))
}
} else if arg != "" && arg != " " {
// Else, if arg is not an environment variable, return it as is
processed = append(processed, arg)
}
}
return
}
// handleCuratedVar - Replace an environment variable alone and without any undesired characters attached
func handleCuratedVar(arg string) (value string) {
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
envVar := strings.TrimPrefix(arg, "$")
val, ok := clientEnv[envVar]
if !ok {
return envVar
}
return val
}
if arg != "" && arg == "~" {
return clientEnv["HOME"]
}
return arg
}
// handleEmbeddedVar - Replace an environment variable that is in the middle of a path, or other one-string combination
func handleEmbeddedVar(arg string) (value string) {
envArgs := strings.Split(arg, "/")
var path []string
for _, arg := range envArgs {
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
envVar := strings.TrimPrefix(arg, "$")
val, ok := clientEnv[envVar]
if !ok {
// Err will be caught when command is ran anyway, or completion will stop...
path = append(path, arg)
}
path = append(path, val)
} else if arg != "" && arg == "~" {
path = append(path, clientEnv["HOME"])
} else if arg != " " && arg != "" {
path = append(path, arg)
}
}
return strings.Join(path, "/")
}
// loadClientEnv - Loads all user environment variables
func loadClientEnv() error {
env := os.Environ()
for _, kv := range env {
key := strings.Split(kv, "=")[0]
value := strings.Split(kv, "=")[1]
clientEnv[key] = value
}
return nil
}

View File

@ -0,0 +1,180 @@
package completers
import (
"strings"
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// HintCompleter - Entrypoint to all hints in the Wiregost console
func (c *CommandCompleter) HintCompleter(line []rune, pos int) (hint []rune) {
// Format and sanitize input
// @args => All items of the input line
// @last => The last word detected in input line as []rune
// @lastWord => The last word detected in input as string
args, last, lastWord := formatInput(line)
// Detect base command automatically
var command = c.detectedCommand(args)
// Menu hints (command line is empty, or nothing recognized)
if noCommandOrEmpty(args, last, command) {
hint = MenuHint(args, last)
}
// Check environment variables
if envVarAsked(args, lastWord) {
return envVarHint(args, last)
}
// Command Hint
if commandFound(command) {
// Command hint by default (no space between cursor and last command character)
hint = CommandHint(command)
// Check environment variables
if envVarAsked(args, lastWord) {
return envVarHint(args, last)
}
// If options are asked for root command, return commpletions.
if len(command.Groups()) > 0 {
for _, grp := range command.Groups() {
if opt, yes := optionArgRequired(args, last, grp); yes {
hint = OptionArgumentHint(args, last, opt)
}
}
}
// If command has args, hint for args
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
hint = []rune(CommandArgumentHints(args, last, command, arg))
}
// Brief subcommand hint
if lastIsSubCommand(lastWord, command) {
hint = []rune(commandHint + command.Find(string(last)).ShortDescription)
}
// Handle subcommand if found
if sub, ok := subCommandFound(lastWord, args, command); ok {
return HandleSubcommandHints(args, last, sub)
}
}
// Handle system binaries, shell commands, etc...
if commandFoundInPath(args[0]) {
// hint = []rune(exeHint + util.ParseSummary(util.GetManPages(args[0])))
}
return
}
// CommandHint - Yields the hint of a Wiregost command
func CommandHint(command *flags.Command) (hint []rune) {
return []rune(commandHint + command.ShortDescription)
}
// HandleSubcommandHints - Handles hints for a subcommand and its arguments, options, etc.
func HandleSubcommandHints(args []string, last []rune, command *flags.Command) (hint []rune) {
// If command has args, hint for args
if arg, yes := commandArgumentRequired(string(last), args, command); yes {
hint = []rune(CommandArgumentHints(args, last, command, arg))
return
}
// Environment variables
if envVarAsked(args, string(last)) {
hint = envVarHint(args, last)
}
// If the last word in input is an option --name, yield argument hint if needed
if len(command.Groups()) > 0 {
for _, grp := range command.Groups() {
if opt, yes := optionArgRequired(args, last, grp); yes {
hint = OptionArgumentHint(args, last, opt)
}
}
}
// If user asks for completions with "-" or "--".
// (Note: This takes precedence on any argument hints, as it is evaluated after them)
if commandOptionsAsked(args, string(last), command) {
return OptionHints(args, last, command)
}
return
}
// CommandArgumentHints - Yields hints for arguments to commands if they have some
func CommandArgumentHints(args []string, last []rune, command *flags.Command, arg string) (hint []rune) {
found := argumentByName(command, arg)
// Base Hint is just a description of the command argument
hint = []rune(argHint + found.Description)
return
}
// ModuleOptionHints - If the option being set has a description, show it
func ModuleOptionHints(opt string) (hint []rune) {
return
}
// OptionHints - Yields hints for proposed options lists/groups
func OptionHints(args []string, last []rune, command *flags.Command) (hint []rune) {
return
}
// OptionArgumentHint - Yields hints for arguments to an option (generally the last word in input)
func OptionArgumentHint(args []string, last []rune, opt *flags.Option) (hint []rune) {
return []rune(valueHint + opt.Description)
}
// MenuHint - Returns the Hint for a given menu context
func MenuHint(args []string, current []rune) (hint []rune) {
return
}
// SpecialCommandHint - Shows hints for Wiregost special commands
func SpecialCommandHint(args []string, current []rune) (hint []rune) {
return current
}
// envVarHint - Yields hints for environment variables
func envVarHint(args []string, last []rune) (hint []rune) {
// Trim last in case its a path with multiple vars
allVars := strings.Split(string(last), "/")
lastVar := allVars[len(allVars)-1]
// Base hint
hint = []rune(envHint + lastVar)
envVar := strings.TrimPrefix(lastVar, "$")
if v, ok := clientEnv[envVar]; ok {
if v != "" {
hintStr := string(hint) + " => " + clientEnv[envVar]
hint = []rune(hintStr)
}
}
return
}
var (
// Hint signs
menuHint = readline.RESET + readline.DIM + readline.BOLD + " menu " + readline.RESET // Dim
envHint = readline.RESET + readline.GREEN + readline.BOLD + " env " + readline.RESET + readline.DIM + readline.GREEN // Green
commandHint = readline.RESET + readline.DIM + readline.BOLD + " command " + readline.RESET + readline.DIM + "\033[38;5;244m" // Cream
exeHint = readline.RESET + readline.DIM + readline.BOLD + " shell " + readline.RESET + readline.DIM // Dim
optionHint = "\033[38;5;222m" + readline.BOLD + " options " + readline.RESET + readline.DIM + "\033[38;5;222m" // Cream-Yellow
valueHint = readline.RESET + readline.DIM + readline.BOLD + " value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
// valueHint = "\033[38;5;217m" + readline.BOLD + " Value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
argHint = readline.DIM + "\033[38;5;217m" + readline.BOLD + " arg " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
)

View File

@ -0,0 +1,205 @@
package completers
import (
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/maxlandon/readline"
)
func completeLocalPath(last string) (string, *readline.CompletionGroup) {
// Completions
completion := &readline.CompletionGroup{
Name: "(console) local path",
MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength
DisplayType: readline.TabDisplayGrid,
TrimSlash: true,
}
var suggestions []string
// Any parsing error is silently ignored, for not messing the prompt
processedPath, _ := ParseEnvironmentVariables([]string{last})
// Check if processed input is empty
var inputPath string
if len(processedPath) == 1 {
inputPath = processedPath[0]
}
// Add a slash if the raw input has one but not the processed input
if len(last) > 0 && last[len(last)-1] == '/' {
inputPath += "/"
}
var linePath string // curated version of the inputPath
var absPath string // absolute path (excluding suffix) of the inputPath
var lastPath string // last directory in the input path
if strings.HasSuffix(string(inputPath), "/") {
linePath = filepath.Dir(string(inputPath))
absPath, _ = expand(string(linePath)) // Get absolute path
} else if string(inputPath) == "" {
linePath = "."
absPath, _ = expand(string(linePath))
} else {
linePath = filepath.Dir(string(inputPath))
absPath, _ = expand(string(linePath)) // Get absolute path
lastPath = filepath.Base(string(inputPath)) // Save filter
}
// 2) We take the absolute path we found, and get all dirs in it.
var dirs []string
files, _ := ioutil.ReadDir(absPath)
for _, file := range files {
if file.IsDir() {
dirs = append(dirs, file.Name())
}
}
switch lastPath {
case "":
for _, dir := range dirs {
if strings.HasPrefix(dir, lastPath) || lastPath == dir {
tokenized := addSpaceTokens(dir)
suggestions = append(suggestions, tokenized+"/")
}
}
default:
filtered := []string{}
for _, dir := range dirs {
if strings.HasPrefix(dir, lastPath) {
filtered = append(filtered, dir)
}
}
for _, dir := range filtered {
if !hasPrefix([]rune(lastPath), []rune(dir)) || lastPath == dir {
tokenized := addSpaceTokens(dir)
suggestions = append(suggestions, tokenized+"/")
}
}
}
completion.Suggestions = suggestions
return string(lastPath), completion
}
func addSpaceTokens(in string) (path string) {
items := strings.Split(in, " ")
for i := range items {
if len(items) == i+1 { // If last one, no char, add and return
path += items[i]
return
}
path += items[i] + "\\ " // By default add space char and roll
}
return
}
func completeLocalPathAndFiles(last string) (string, *readline.CompletionGroup) {
// Completions
completion := &readline.CompletionGroup{
Name: "(console) local directory/files",
MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength
DisplayType: readline.TabDisplayGrid,
TrimSlash: true,
}
var suggestions []string
// Any parsing error is silently ignored, for not messing the prompt
processedPath, _ := ParseEnvironmentVariables([]string{last})
// Check if processed input is empty
var inputPath string
if len(processedPath) == 1 {
inputPath = processedPath[0]
}
// Add a slash if the raw input has one but not the processed input
if len(last) > 0 && last[len(last)-1] == '/' {
inputPath += "/"
}
var linePath string // curated version of the inputPath
var absPath string // absolute path (excluding suffix) of the inputPath
var lastPath string // last directory in the input path
if strings.HasSuffix(string(inputPath), "/") {
linePath = filepath.Dir(string(inputPath)) // Trim the non needed slash
absPath, _ = expand(string(linePath)) // Get absolute path
} else if string(inputPath) == "" {
linePath = "."
absPath, _ = expand(string(linePath))
} else {
linePath = filepath.Dir(string(inputPath))
absPath, _ = expand(string(linePath)) // Get absolute path
lastPath = filepath.Base(string(inputPath)) // Save filter
}
// 2) We take the absolute path we found, and get all dirs in it.
var dirs []string
files, _ := ioutil.ReadDir(absPath)
for _, file := range files {
if file.IsDir() {
dirs = append(dirs, file.Name())
}
}
switch lastPath {
case "":
for _, file := range files {
if strings.HasPrefix(file.Name(), lastPath) || lastPath == file.Name() {
if file.IsDir() {
suggestions = append(suggestions, file.Name()+"/")
} else {
suggestions = append(suggestions, file.Name())
}
}
}
default:
filtered := []os.FileInfo{}
for _, file := range files {
if strings.HasPrefix(file.Name(), lastPath) {
filtered = append(filtered, file)
}
}
for _, file := range filtered {
if !hasPrefix([]rune(lastPath), []rune(file.Name())) || lastPath == file.Name() {
if file.IsDir() {
suggestions = append(suggestions, file.Name()+"/")
} else {
suggestions = append(suggestions, file.Name())
}
}
}
}
completion.Suggestions = suggestions
return string(lastPath), completion
}
// expand will expand a path with ~ to the $HOME of the current user.
func expand(path string) (string, error) {
if path == "" {
return path, nil
}
home := os.Getenv("HOME")
if home == "" {
usr, err := user.Current()
if err != nil {
return "", err
}
home = usr.HomeDir
}
return filepath.Abs(strings.Replace(path, "~", home, 1))
}

View File

@ -0,0 +1,77 @@
package completers
import (
"strings"
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// completeOptionArguments - Completes all values for arguments to a command. Arguments here are different from command options (--option).
// Many categories, from multiple sources in multiple contexts
func completeOptionArguments(cmd *flags.Command, opt *flags.Option, lastWord string) (prefix string, completions []*readline.CompletionGroup) {
// By default the last word is the prefix
prefix = lastWord
var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions
// First of all: some options, no matter their contexts and subject, have default values.
// When we have such an option, we don't bother analyzing context, we just build completions and return.
if len(opt.Choices) > 0 {
comp = &readline.CompletionGroup{
Name: opt.ValueName, // Value names are specified in struct metadata fields
DisplayType: readline.TabDisplayGrid,
}
for _, choice := range opt.Choices {
if strings.HasPrefix(choice, lastWord) {
comp.Suggestions = append(comp.Suggestions, choice)
}
}
completions = append(completions, comp)
return
}
// EXAMPLE OF COMPLETING ARGUMENTS BASED ON THEIR NAMES -----------------------------------------------------------------------
// We have 3 words, potentially different, with which we can filter:
//
// 1) '--option-name' is the string typed as input.
// 2) 'OptionName' is the name of the struct/type for this option.
// 3) 'ValueName' is the name of the value we expect.
// var match = func(name string) bool {
// if strings.Contains(opt.Field().Name, name) {
// return true
// }
// return false
// }
//
// // Sessions
// if match("ImplantID") || match("SessionID") {
// completions = append(completions, sessionIDs(lastWord))
// }
//
// // Any arguments with a path name. Often we "save" files that need paths, certificates, etc
// if match("Path") || match("Save") || match("Certificate") || match("PrivateKey") {
// switch cmd.Name {
// case constants.WebContentTypeStr, constants.WebUpdateStr, constants.AddWebContentStr, constants.RmWebContentStr:
// // Make an exception for WebPath option in websites commands.
// default:
// switch opt.ValueName {
// case "local-path", "path":
// prefix, comp = completeLocalPath(lastWord)
// completions = append(completions, comp)
// case "local-file", "file":
// prefix, comp = completeLocalPathAndFiles(lastWord)
// completions = append(completions, comp)
// default:
// // We always have a default searching for files, locally
// prefix, comp = completeLocalPathAndFiles(lastWord)
// completions = append(completions, comp)
// }
//
// }
// }
//
return
}

View File

@ -0,0 +1,548 @@
package completers
import (
"os/exec"
"reflect"
"strings"
"unicode"
"github.com/jessevdk/go-flags"
)
// These functions are just shorthands for checking various conditions on the input line.
// They make the main function more readable, which might be useful, should a logic error pop somewhere.
// [ Parser Commands & Options ] --------------------------------------------------------------------------
// ArgumentByName Get the name of a detected command's argument
func argumentByName(command *flags.Command, name string) *flags.Arg {
args := command.Args()
for _, arg := range args {
if arg.Name == name {
return arg
}
}
return nil
}
// optionByName - Returns an option for a command or a subcommand, identified by name
func optionByName(cmd *flags.Command, option string) *flags.Option {
if cmd == nil {
return nil
}
// Get all (root) option groups.
groups := cmd.Groups()
// For each group, build completions
for _, grp := range groups {
// Add each option to completion group
for _, opt := range grp.Options() {
if opt.LongName == option {
return opt
}
}
}
return nil
}
// [ Menus ] --------------------------------------------------------------------------------------------
// Is the input line is either empty, or without any detected command ?
func noCommandOrEmpty(args []string, last []rune, command *flags.Command) bool {
if len(args) == 0 || len(args) == 1 && command == nil {
return true
}
return false
}
// [ Commands ] -------------------------------------------------------------------------------------
// detectedCommand - Returns the base command from parser if detected, depending on context
func (c *CommandCompleter) detectedCommand(args []string) (command *flags.Command) {
arg := strings.TrimSpace(args[0])
command = c.parser.Find(arg)
return
}
// is the command a special command, usually not handled by parser ?
func isSpecialCommand(args []string, command *flags.Command) bool {
// If command is not nil, return
if command == nil {
// Shell
if args[0] == "!" {
return true
}
// Exit
if args[0] == "exit" {
return true
}
return false
}
return false
}
// The commmand has been found
func commandFound(command *flags.Command) bool {
if command != nil {
return true
}
return false
}
// Search for input in $PATH
func commandFoundInPath(input string) bool {
_, err := exec.LookPath(input)
if err != nil {
return false
}
return true
}
// [ SubCommands ]-------------------------------------------------------------------------------------
// Does the command have subcommands ?
func hasSubCommands(command *flags.Command, args []string) bool {
if len(args) < 2 || command == nil {
return false
}
if len(command.Commands()) != 0 {
return true
}
return false
}
// Does the input has a subcommand in it ?
func subCommandFound(lastWord string, raw []string, command *flags.Command) (sub *flags.Command, ok bool) {
// First, filter redundant spaces. This does not modify the actual line
args := ignoreRedundantSpaces(raw)
if len(args) <= 1 || command == nil {
return nil, false
}
sub = command.Find(args[1])
if sub != nil {
return sub, true
}
return nil, false
}
// Is the last input PRECISELY a subcommand. This is used as a brief hint for the subcommand
func lastIsSubCommand(lastWord string, command *flags.Command) bool {
if sub := command.Find(lastWord); sub != nil {
return true
}
return false
}
// [ Arguments ]-------------------------------------------------------------------------------------
// Does the command have arguments ?
func hasArgs(command *flags.Command) bool {
if len(command.Args()) != 0 {
return true
}
return false
}
// commandArgumentRequired - Analyses input and sends back the next argument name to provide completion for
func commandArgumentRequired(lastWord string, raw []string, command *flags.Command) (name string, yes bool) {
// First, filter redundant spaces. This does not modify the actual line
args := ignoreRedundantSpaces(raw)
// Trim command and subcommand args
var remain []string
if args[0] == command.Name {
remain = args[1:]
}
if len(args) > 1 && args[1] == command.Name {
remain = args[2:]
}
// The remain may include a "" as a last element,
// which we don't consider as a real remain, so we move it away
switch lastWord {
case "":
case command.Name:
return "", false
}
// Trim all --option flags and their arguments if they have
remain = filterOptions(remain, command)
// For each argument, check if needs completion. If not continue, if yes return.
// The arguments remainder is popped according to the number of values expected.
for i, arg := range command.Args() {
// If it's required and has one argument, check filled.
if arg.Required == 1 && arg.RequiredMaximum == 1 {
// If last word is the argument, and we are
// last arg in: line keep completing.
if len(remain) < 1 {
return arg.Name, true
}
// If the we are still writing the argument
if len(remain) == 1 {
if lastWord != "" {
return arg.Name, true
}
}
// If filed and we are not last arg, continue
if len(remain) > 1 && i < (len(command.Args())-1) {
remain = remain[1:]
continue
}
continue
}
// If we need more than one value and we knwo the maximum,
// either return or pop the remain.
if arg.Required > 0 && arg.RequiredMaximum > 1 {
// Pop the corresponding amount of arguments.
var found int
for i := 0; i < len(remain) && i < arg.RequiredMaximum; i++ {
remain = remain[1:]
found++
}
// If we still need values:
if len(remain) == 0 && found <= arg.RequiredMaximum {
if lastWord == "" { // We are done, no more completions.
break
} else {
return arg.Name, true
}
}
// Else go on with the next argument
continue
}
// If has required arguments, with no limit of needs, return true
if arg.Required > 0 && arg.RequiredMaximum == -1 {
return arg.Name, true
}
// Else, if no requirements and the command has subcommands,
// return so that we complete subcommands
if arg.Required == -1 && len(command.Commands()) > 0 {
continue
}
// Else, return this argument
// NOTE: This block is after because we always use []type arguments
// AFTER individual argument fields. Thus blocks any args that have
// not been processed.
if arg.Required == -1 {
return arg.Name, true
}
}
// Once we exited the loop, it means that none of the arguments require completion:
// They are all either optional, or fullfiled according to their required numbers.
// Thus we return none
return "", false
}
// getRemainingArgs - Filters the input slice from commands and detected option:value pairs, and returns args
func getRemainingArgs(args []string, last []rune, command *flags.Command) (remain []string) {
var input []string
// Clean subcommand name
if args[0] == command.Name && len(args) >= 2 {
input = args[1:]
} else if len(args) == 1 {
input = args
}
// For each each argument
for i := 0; i < len(input); i++ {
// Check option prefix
if strings.HasPrefix(input[i], "-") || strings.HasPrefix(input[i], "--") {
// Clean it
cur := strings.TrimPrefix(input[i], "--")
cur = strings.TrimPrefix(cur, "-")
// Check if option matches any command option
if opt := command.FindOptionByLongName(cur); opt != nil {
boolean := true
if opt.Field().Type == reflect.TypeOf(boolean) {
continue // If option is boolean, don't skip an argument
}
i++ // Else skip next arg in input
continue
}
}
// Safety check
if input[i] == "" || input[i] == " " {
continue
}
remain = append(remain, input[i])
}
return
}
// [ Options ]-------------------------------------------------------------------------------------
// commandOptionsAsked - Does the user asks for options in a root command ?
func commandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
if len(args) >= 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
return true
}
return false
}
// commandOptionsAsked - Does the user asks for options in a subcommand ?
func subCommandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
if len(args) > 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
return true
}
return false
}
// Is the last input argument is a dash ?
func isOptionDash(args []string, last []rune) bool {
if len(args) > 2 && (strings.HasPrefix(string(last), "-") || strings.HasPrefix(string(last), "--")) {
return true
}
return false
}
// optionIsAlreadySet - Detects in input if an option is already set
func optionIsAlreadySet(args []string, lastWord string, opt *flags.Option) bool {
return false
}
// Check if option type allows for repetition
func optionNotRepeatable(opt *flags.Option) bool {
return true
}
// [ Option Values ]-------------------------------------------------------------------------------------
// Is the last input word an option name (--option) ?
func optionArgRequired(args []string, last []rune, group *flags.Group) (opt *flags.Option, yes bool) {
var lastItem string
var lastOption string
var option *flags.Option
// If there is argument required we must have 1) command 2) --option inputs at least.
if len(args) <= 2 {
return nil, false
}
// Check for last two arguments in input
if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {
// Long opts
if strings.HasPrefix(args[len(args)-2], "--") {
lastOption = strings.TrimPrefix(args[len(args)-2], "--")
if opt := group.FindOptionByLongName(lastOption); opt != nil {
option = opt
}
// Short opts
} else if strings.HasPrefix(args[len(args)-2], "-") {
lastOption = strings.TrimPrefix(args[len(args)-2], "-")
if len(lastOption) > 0 {
if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
option = opt
}
}
}
}
// If option is found, and we still are in writing the argument
if (lastItem == "" && option != nil) || option != nil {
// Check if option is a boolean, if yes return false
boolean := true
if option.Field().Type == reflect.TypeOf(boolean) {
return nil, false
}
return option, true
}
// Check for previous argument
if lastItem != "" && option == nil {
if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {
// Long opts
if strings.HasPrefix(args[len(args)-2], "--") {
lastOption = strings.TrimPrefix(args[len(args)-2], "--")
if opt := group.FindOptionByLongName(lastOption); opt != nil {
option = opt
return option, true
}
// Short opts
} else if strings.HasPrefix(args[len(args)-2], "-") {
lastOption = strings.TrimPrefix(args[len(args)-2], "-")
if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
option = opt
return option, true
}
}
}
}
return nil, false
}
// [ Other ]-------------------------------------------------------------------------------------
// Does the user asks for Environment variables ?
func envVarAsked(args []string, lastWord string) bool {
// Check if the current word is an environment variable, or if the last part of it is a variable
if len(lastWord) > 1 && strings.HasPrefix(lastWord, "$") {
if strings.LastIndex(lastWord, "/") < strings.LastIndex(lastWord, "$") {
return true
}
return false
}
// Check if env var is asked in a path or something
if len(lastWord) > 1 {
// If last is a path, it cannot be an env var anymore
if lastWord[len(lastWord)-1] == '/' {
return false
}
if lastWord[len(lastWord)-1] == '$' {
return true
}
}
// If we are at the beginning of an env var
if len(lastWord) > 0 && lastWord[len(lastWord)-1] == '$' {
return true
}
return false
}
// filterOptions - Check various elements of an option and return a list
func filterOptions(args []string, command *flags.Command) (processed []string) {
for i := 0; i < len(args); i++ {
arg := args[i]
// --long-name options
if strings.HasPrefix(arg, "--") {
name := strings.TrimPrefix(arg, "--")
if opt := optionByName(command, name); opt != nil {
var boolean = true
if opt.Field().Type == reflect.TypeOf(boolean) {
continue
}
// Else skip the option argument (next item)
i++
}
continue
}
// -s short options
if strings.HasPrefix(arg, "-") {
name := strings.TrimPrefix(arg, "-")
if opt := optionByName(command, name); opt != nil {
var boolean = true
if opt.Field().Type == reflect.TypeOf(boolean) {
continue
}
// Else skip the option argument (next item)
i++
}
continue
}
processed = append(processed, arg)
}
return
}
// Other Functions -------------------------------------------------------------------------------------------------------------//
// formatInput - Formats & sanitize the command line input
func formatInput(line []rune) (args []string, last []rune, lastWord string) {
args = strings.Split(string(line), " ") // The readline input as a []string
last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
lastWord = string(last)
return
}
// FormatInput - Formats & sanitize the command line input
func formatInputHighlighter(line []rune) (args []string, last []rune, lastWord string) {
args = strings.SplitN(string(line), " ", -1)
last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
lastWord = string(last)
return
}
// ignoreRedundantSpaces - We might have several spaces between each real arguments.
// However these indivual spaces are counted as args themselves.
// For each space arg found, verify that no space args follow,
// and if some are found, delete them.
func ignoreRedundantSpaces(raw []string) (args []string) {
for i := 0; i < len(raw); i++ {
// Catch a space argument.
if raw[i] == "" {
// The arg evaulated is always kept, because we just adjusted
// the indexing to avoid the ones we don't need
// args = append(args, raw[i])
for y, next := range raw[i:] {
if next != "" {
i += y - 1
break
}
// If we come to the end while not breaking
// we push the outer loop straight to the end.
if y == len(raw[i:])-1 {
i += y
}
}
} else {
// The arg evaulated is always kept, because we just adjusted
// the indexing to avoid the ones we don't need
args = append(args, raw[i])
}
}
return
}
func trimSpaceLeft(in []rune) []rune {
firstIndex := len(in)
for i, r := range in {
if unicode.IsSpace(r) == false {
firstIndex = i
break
}
}
return in[firstIndex:]
}
func equal(a, b []rune) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
func hasPrefix(r, prefix []rune) bool {
if len(r) < len(prefix) {
return false
}
return equal(r[:len(prefix)], prefix)
}

View File

@ -0,0 +1,151 @@
package completers
import (
"fmt"
"strings"
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// SyntaxHighlighter - Entrypoint to all input syntax highlighting in the Wiregost console
func (c *CommandCompleter) SyntaxHighlighter(input []rune) (line string) {
// Format and sanitize input
args, last, lastWord := formatInputHighlighter(input)
// Remain is all arguments that have not been highlighted, we need it for completing long commands
var remain = args
// Detect base command automatically
var command = c.detectedCommand(args)
// Return input as is
if noCommandOrEmpty(remain, last, command) {
return string(input)
}
// Base command
if commandFound(command) {
line, remain = highlightCommand(remain, command)
// SubCommand
if sub, ok := subCommandFound(lastWord, args, command); ok {
line, remain = highlightSubCommand(line, remain, sub)
}
}
line = processRemain(line, remain)
return
}
func highlightCommand(args []string, command *flags.Command) (line string, remain []string) {
line = readline.BOLD + args[0] + readline.RESET + " "
remain = args[1:]
return
}
func highlightSubCommand(input string, args []string, command *flags.Command) (line string, remain []string) {
line = input
line += readline.BOLD + args[0] + readline.RESET + " "
remain = args[1:]
return
}
func processRemain(input string, remain []string) (line string) {
// Check the last is not the last space in input
if len(remain) == 1 && remain[0] == " " {
return input
}
line = input + strings.Join(remain, " ")
// line = processEnvVars(input, remain)
return
}
// processEnvVars - Highlights environment variables. NOTE: Rewrite with logic from console/env.go
func processEnvVars(input string, remain []string) (line string) {
var processed []string
inputSlice := strings.Split(input, " ")
// Check already processed input
for _, arg := range inputSlice {
if arg == "" || arg == " " {
continue
}
if strings.HasPrefix(arg, "$") { // It is an env var.
if args := strings.Split(arg, "/"); len(args) > 1 {
for _, a := range args {
fmt.Println(a)
if strings.HasPrefix(a, "$") && a != " " { // It is an env var.
processed = append(processed, "\033[38;5;108m"+readline.DIM+a+readline.RESET)
continue
}
}
}
processed = append(processed, "\033[38;5;108m"+readline.DIM+arg+readline.RESET)
continue
}
processed = append(processed, arg)
}
// Check remaining args (non-processed)
for _, arg := range remain {
if arg == "" {
continue
}
if strings.HasPrefix(arg, "$") && arg != "$" { // It is an env var.
var full string
args := strings.Split(arg, "/")
if len(args) == 1 {
if strings.HasPrefix(args[0], "$") && args[0] != "" && args[0] != "$" { // It is an env var.
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET
continue
}
}
if len(args) > 1 {
var counter int
for _, arg := range args {
// If var is an env var
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
if counter < len(args)-1 {
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET + "/"
counter++
continue
}
if counter == len(args)-1 {
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET
counter++
continue
}
}
// Else, if we are not at the end of array
if counter < len(args)-1 && arg != "" {
full += arg + "/"
counter++
}
if counter == len(args)-1 {
full += arg
counter++
}
}
}
// Else add first var
processed = append(processed, full)
}
}
line = strings.Join(processed, " ")
// Very important, keeps the line clear when erasing
// line += " "
return
}

View File

@ -0,0 +1,289 @@
package completers
import (
"errors"
"fmt"
"strings"
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// CommandCompleter - A completer using a github.com/jessevdk/go-flags Command Parser, in order
// to build completions for commands, arguments, options and their arguments as well.
// This completer needs to be instantiated with its constructor, in order to ensure the parser is not nil.
type CommandCompleter struct {
parser *flags.Parser
}
// NewCommandCompleter - Instantiate a new tab completer using a github.com/jessevdk/go-flags Command Parser.
func NewCommandCompleter(parser *flags.Parser) (completer *CommandCompleter, err error) {
if parser == nil {
return nil, errors.New("command completer was instantiated with a nil parser")
}
return &CommandCompleter{parser: parser}, nil
}
// TabCompleter - A default tab completer working with a github.com/jessevdk/go-flags parser.
func (c *CommandCompleter) TabCompleter(line []rune, pos int, dtc readline.DelayedTabContext) (lastWord string, completions []*readline.CompletionGroup) {
// Format and sanitize input
// @args => All items of the input line
// @last => The last word detected in input line as []rune
// @lastWord => The last word detected in input as string
args, last, lastWord := formatInput(line)
// Detect base command automatically
var command = c.detectedCommand(args)
// Propose commands
if noCommandOrEmpty(args, last, command) {
return c.completeMenuCommands(lastWord, pos)
}
// Check environment variables
if envVarAsked(args, lastWord) {
completeEnvironmentVariables(lastWord)
}
// Base command has been identified
if commandFound(command) {
// Check environment variables again
if envVarAsked(args, lastWord) {
return completeEnvironmentVariables(lastWord)
}
// If options are asked for root command, return commpletions.
if len(command.Groups()) > 0 {
for _, grp := range command.Groups() {
if opt, yes := optionArgRequired(args, last, grp); yes {
return completeOptionArguments(command, opt, lastWord)
}
}
}
// Then propose subcommands. We don't return from here, otherwise it always skips the next steps.
if hasSubCommands(command, args) {
completions = completeSubCommands(args, lastWord, command)
}
// Handle subcommand if found (maybe we should rewrite this function and use it also for base command)
if sub, ok := subCommandFound(lastWord, args, command); ok {
return handleSubCommand(line, pos, sub)
}
// If user asks for completions with "-" / "--", show command options.
// We ask this here, after having ensured there is no subcommand invoked.
// This prevails over command arguments, even if they are required.
if commandOptionsAsked(args, lastWord, command) {
return completeCommandOptions(args, lastWord, command)
}
// Propose argument completion before anything, and if needed
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
return completeCommandArguments(command, arg, lastWord)
}
}
return
}
// [ Main Completion Functions ] -----------------------------------------------------------------------------------------------------------------
// completeMenuCommands - Selects all commands available in a given context and returns them as suggestions
// Many categories, all from command parsers.
func (c *CommandCompleter) completeMenuCommands(lastWord string, pos int) (prefix string, completions []*readline.CompletionGroup) {
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
// Check their namespace (which should be their "group" (like utils, core, Jobs, etc))
for _, cmd := range c.parser.Commands() {
// If command matches readline input
if strings.HasPrefix(cmd.Name, lastWord) {
// Check command group: add to existing group if found
var found bool
for _, grp := range completions {
if grp.Name == cmd.Aliases[0] {
found = true
grp.Suggestions = append(grp.Suggestions, cmd.Name)
grp.Descriptions[cmd.Name] = readline.Dim(cmd.ShortDescription)
}
}
// Add a new group if not found
if !found {
grp := &readline.CompletionGroup{
Name: cmd.Aliases[0],
Suggestions: []string{cmd.Name},
Descriptions: map[string]string{
cmd.Name: readline.Dim(cmd.ShortDescription),
},
}
completions = append(completions, grp)
}
}
}
// Make adjustments to the CompletionGroup list: set maxlength depending on items, check descriptions, etc.
for _, grp := range completions {
// If the length of suggestions is too long and we have
// many groups, use grid display.
if len(completions) >= 10 && len(grp.Suggestions) >= 7 {
grp.DisplayType = readline.TabDisplayGrid
} else {
// By default, we use a map of command to descriptions
grp.DisplayType = readline.TabDisplayList
}
}
return
}
// completeSubCommands - Takes subcommands and gives them as suggestions
// One category, from one source (a parent command).
func completeSubCommands(args []string, lastWord string, command *flags.Command) (completions []*readline.CompletionGroup) {
group := &readline.CompletionGroup{
Name: command.Name,
Suggestions: []string{},
Descriptions: map[string]string{},
DisplayType: readline.TabDisplayList,
}
for _, sub := range command.Commands() {
if strings.HasPrefix(sub.Name, lastWord) {
group.Suggestions = append(group.Suggestions, sub.Name)
group.Descriptions[sub.Name] = readline.DIM + sub.ShortDescription + readline.RESET
}
}
completions = append(completions, group)
return
}
// handleSubCommand - Handles completion for subcommand options and arguments, + any option value related completion
// Many categories, from many sources: this function calls the same functions as the ones previously called for completing its parent command.
func handleSubCommand(line []rune, pos int, command *flags.Command) (lastWord string, completions []*readline.CompletionGroup) {
args, last, lastWord := formatInput(line)
// Check environment variables
if envVarAsked(args, lastWord) {
completeEnvironmentVariables(lastWord)
}
// Check argument options
if len(command.Groups()) > 0 {
for _, grp := range command.Groups() {
if opt, yes := optionArgRequired(args, last, grp); yes {
return completeOptionArguments(command, opt, lastWord)
}
}
}
// If user asks for completions with "-" or "--". This must take precedence on arguments.
if subCommandOptionsAsked(args, lastWord, command) {
return completeCommandOptions(args, lastWord, command)
}
// If command has non-filled arguments, propose them first
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
return completeCommandArguments(command, arg, lastWord)
}
return
}
// completeCommandOptions - Yields completion for options of a command, with various decorators
// Many categories, from one source (a command)
func completeCommandOptions(args []string, lastWord string, cmd *flags.Command) (prefix string, completions []*readline.CompletionGroup) {
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
// Get all (root) option groups.
groups := cmd.Groups()
// Append command options not gathered in groups
groups = append(groups, cmd.Group)
// For each group, build completions
for _, grp := range groups {
_, comp := completeOptionGroup(lastWord, grp, "")
// No need to add empty groups, will screw the completion system.
if len(comp.Suggestions) > 0 {
completions = append(completions, comp)
}
}
// Do the same for global options, which are not part of any group "per-se"
_, gcomp := completeOptionGroup(lastWord, cmd.Group, "global options")
if len(gcomp.Suggestions) > 0 {
completions = append(completions, gcomp)
}
return
}
// completeOptionGroup - make completions for a single group of options. Title is optional, not used if empty.
func completeOptionGroup(lastWord string, grp *flags.Group, title string) (prefix string, compGrp *readline.CompletionGroup) {
compGrp = &readline.CompletionGroup{
Name: grp.ShortDescription,
Descriptions: map[string]string{},
DisplayType: readline.TabDisplayList,
Aliases: map[string]string{},
}
// An optional title for this comp group.
// Used by global flag options, added to all commands.
if title != "" {
compGrp.Name = title
}
// Add each option to completion group
for _, opt := range grp.Options() {
// Check if option is already set, next option if yes
// if optionNotRepeatable(opt) && optionIsAlreadySet(args, lastWord, opt) {
// continue
// }
// Depending on the current last word, either build a group with option longs only, or with shorts
if strings.HasPrefix("--"+opt.LongName, lastWord) {
optName := "--" + opt.LongName
compGrp.Suggestions = append(compGrp.Suggestions, optName)
// Add short if there is, and that the prefix is only one dash
if strings.HasPrefix("-", lastWord) {
if opt.ShortName != 0 {
compGrp.Aliases[optName] = "-" + string(opt.ShortName)
}
}
// Option default value if any
var def string
if len(opt.Default) > 0 {
def = " (default:"
for _, d := range opt.Default {
def += " " + d + ","
}
def = strings.TrimSuffix(def, ",")
def += ")"
}
desc := fmt.Sprintf(" -- %s%s%s", opt.Description, def, readline.RESET)
compGrp.Descriptions[optName] = desc
}
}
return
}
// RecursiveGroupCompletion - Handles recursive completion for nested option groups
// Many categories, one source (a command's root option group). Called by the function just above.
func RecursiveGroupCompletion(args []string, last []rune, group *flags.Group) (lastWord string, completions []*readline.CompletionGroup) {
return
}

View File

@ -1,7 +1,7 @@
package readline
import (
// "fmt"
// "fmt"
"os"
"regexp"
"strconv"
@ -28,7 +28,7 @@ func leftMost() []byte {
var rxRcvCursorPos = regexp.MustCompile("^\x1b([0-9]+);([0-9]+)R$")
func (rl *Readline) getCursorPos() (x int, y int) {
func (rl *Instance) getCursorPos() (x int, y int) {
if !rl.EnableGetCursorPos {
return -1, -1
}
@ -143,7 +143,7 @@ func unhideCursor() {
print(seqUnhideCursor)
}
func (rl *Readline) backspace(forward bool) {
func (rl *Instance) backspace(forward bool) {
if len(rl.line) == 0 || rl.pos == 0 {
return
}
@ -151,7 +151,7 @@ func (rl *Readline) backspace(forward bool) {
rl.deleteBackspace(forward)
}
func (rl *Readline) moveCursorByAdjust(adjust int) {
func (rl *Instance) moveCursorByAdjust(adjust int) {
switch {
case adjust > 0:
rl.pos += adjust

View File

@ -14,7 +14,7 @@ import (
)
// writeTempFile - This function optionally accepts a filename (generally specified with an extension).
func (rl *Readline) writeTempFile(content []byte, filename string) (string, error) {
func (rl *Instance) writeTempFile(content []byte, filename string) (string, error) {
// The final path to the buffer on disk
var path string

View File

@ -1,10 +1,9 @@
//go:build plan9
// +build plan9
package readline
import "errors"
func (rl *Readline) launchEditor(multiline []rune) ([]rune, error) {
func (rl *Instance) launchEditor(multiline []rune) ([]rune, error) {
return rl.line, errors.New("Not currently supported on Plan 9")
}

View File

@ -15,7 +15,7 @@ const defaultEditor = "vi"
// depending on the actions taken by the user within it (eg: x or q! in Vim)
// The filename parameter can be used to pass a specific filename.ext pattern,
// which might be useful if the editor has builtin filetype plugin functionality.
func (rl *Readline) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
func (rl *Instance) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
name, err := rl.writeTempFile([]byte(string(multiline)), filename)
if err != nil {
return multiline, err

View File

@ -5,6 +5,6 @@ package readline
import "errors"
// StartEditorWithBuffer - Not implemented on Windows platforms.
func (rl *Readline) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
func (rl *Instance) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
return rl.line, errors.New("Not currently supported on Windows")
}

View File

@ -13,11 +13,11 @@ type EventReturn struct {
}
// AddEvent registers a new keypress handler
func (rl *Readline) AddEvent(keyPress string, callback func(string, []rune, int) *EventReturn) {
func (rl *Instance) AddEvent(keyPress string, callback func(string, []rune, int) *EventReturn) {
rl.evtKeyPress[keyPress] = callback
}
// DelEvent deregisters an existing keypress handler
func (rl *Readline) DelEvent(keyPress string) {
func (rl *Instance) DelEvent(keyPress string) {
delete(rl.evtKeyPress, keyPress)
}

View File

@ -0,0 +1,109 @@
package main
// This file defines a few argument choices for commands
import (
"github.com/jessevdk/go-flags"
)
// Command/option argument choices
var (
// Logs & components
logLevels = []string{"trace", "debug", "info", "warning", "error"}
loggers = []string{"client", "comm"}
// Stages / Stagers
implantOS = []string{"windows", "linux", "darwin"}
implantArch = []string{"amd64", "x86"}
implantFmt = []string{"exe", "shared", "service", "shellcode"}
stageListenerProtocols = []string{"tcp", "http", "https"}
// MSF
msfStagerProtocols = []string{"tcp", "http", "https"}
msfTransformFormats = []string{
"bash",
"c",
"csharp",
"dw",
"dword",
"hex",
"java",
"js_be",
"js_le",
"num",
"perl",
"pl",
"powershell",
"ps1",
"py",
"python",
"raw",
"rb",
"ruby",
"sh",
"vbapplication",
"vbscript",
}
msfEncoders = []string{
"x86/shikata_ga_nai",
"x64/xor_dynamic",
}
msfPayloads = map[string][]string{
"windows": windowsMsfPayloads,
"linux": linuxMsfPayloads,
"osx": osxMsfPayloads,
}
// ValidPayloads - Valid payloads and OS combos
windowsMsfPayloads = []string{
"meterpreter_reverse_http",
"meterpreter_reverse_https",
"meterpreter_reverse_tcp",
"meterpreter/reverse_tcp",
"meterpreter/reverse_http",
"meterpreter/reverse_https",
}
linuxMsfPayloads = []string{
"meterpreter_reverse_http",
"meterpreter_reverse_https",
"meterpreter_reverse_tcp",
}
osxMsfPayloads = []string{
"meterpreter_reverse_http",
"meterpreter_reverse_https",
"meterpreter_reverse_tcp",
}
// Comm network protocols
portfwdProtocols = []string{"tcp", "udp"}
transportProtocols = []string{"tcp", "udp", "ip"}
applicationProtocols = []string{"http", "https", "mtls", "quic", "http3", "dns", "named_pipe"}
)
// loadArgumentCompletions - Adds a bunch of choices for command arguments (and their completions.)
func loadArgumentCompletions(parser *flags.Parser) {
if parser == nil {
return
}
serverCompsAddtional(parser)
}
// Additional completion mappings for command in the server context
func serverCompsAddtional(parser *flags.Parser) {
// Stage options
g := parser.Find("generate")
g.FindOptionByLongName("os").Choices = implantOS
g.FindOptionByLongName("arch").Choices = implantArch
g.FindOptionByLongName("format").Choices = implantFmt
// Stager options (mostly MSF)
gs := g.Find("stager")
gs.FindOptionByLongName("os").Choices = implantOS
gs.FindOptionByLongName("arch").Choices = implantArch
gs.FindOptionByLongName("protocol").Choices = msfStagerProtocols
gs.FindOptionByLongName("msf-format").Choices = msfTransformFormats
}

View File

@ -0,0 +1,315 @@
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
)
// This file declares a go-flags parser and a few commands.
var (
// commandParser - The command parser used by the example console.
commandParser = flags.NewNamedParser("example", flags.IgnoreUnknown)
)
func bindCommands() (err error) {
// core console
// ----------------------------------------------------------------------------------------
ex, err := commandParser.AddCommand("exit", // Command string
"Exit from the client/server console", // Description (completions, help usage)
"", // Long description
&Exit{}) // Command implementation
ex.Aliases = []string{"core"}
cd, err := commandParser.AddCommand("cd",
"Change client working directory",
"",
&ChangeClientDirectory{})
cd.Aliases = []string{"core"}
ls, err := commandParser.AddCommand("ls",
"List directory contents",
"",
&ListClientDirectories{})
ls.Aliases = []string{"core"}
// Log
log, err := commandParser.AddCommand("log",
"Manage log levels of one or more components",
"",
&Log{})
log.Aliases = []string{"core"}
// Implant generation
// ----------------------------------------------------------------------------------------
g, err := commandParser.AddCommand("generate",
"Configure and compile an implant (staged or stager)",
"",
&Generate{})
g.Aliases = []string{"builds"}
g.SubcommandsOptional = true
_, err = g.AddCommand("stager",
"Generate a stager shellcode payload using MSFVenom, (to file: --save, to stdout: --format",
"",
&GenerateStager{})
r, err := commandParser.AddCommand("regenerate",
"Recompile an implant by name, passed as argument (completed)",
"",
&Regenerate{})
r.Aliases = []string{"builds"}
// Add choices completions (and therefore completions) to some of these commands.
loadArgumentCompletions(commandParser)
return
}
// Exit - Kill the current client console
type Exit struct{}
// Execute - Run
func (e *Exit) Execute(args []string) (err error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Confirm exit (Y/y): ")
text, _ := reader.ReadString('\n')
answer := strings.TrimSpace(text)
if (answer == "Y") || (answer == "y") {
os.Exit(0)
}
fmt.Println()
return
}
// ChangeClientDirectory - Change the working directory of the client console
type ChangeClientDirectory struct {
Positional struct {
Path string `description:"local path" required:"1-1"`
} `positional-args:"yes" required:"yes"`
}
// Execute - Handler for ChangeDirectory
func (cd *ChangeClientDirectory) Execute(args []string) (err error) {
dir, err := expand(cd.Positional.Path)
err = os.Chdir(dir)
if err != nil {
fmt.Printf(CommandError+"%s \n", err)
} else {
fmt.Printf(Info+"Changed directory to %s \n", dir)
}
return
}
// ListClientDirectories - List directory contents
type ListClientDirectories struct {
Positional struct {
Path []string `description:"local directory/file"`
} `positional-args:"yes"`
}
// Execute - Command
func (ls *ListClientDirectories) Execute(args []string) error {
base := []string{"ls", "--color", "-l"}
if len(ls.Positional.Path) == 0 {
ls.Positional.Path = []string{"."}
}
fullPaths := []string{}
for _, path := range ls.Positional.Path {
full, _ := expand(path)
fullPaths = append(fullPaths, full)
}
base = append(base, fullPaths...)
// Print output
out, err := shellExec(base[0], base[1:])
if err != nil {
fmt.Printf(CommandError+"%s \n", err.Error())
return nil
}
// Print output
fmt.Println(out)
return nil
}
// shellExec - Execute a program
func shellExec(executable string, args []string) (string, error) {
path, err := exec.LookPath(executable)
if err != nil {
return "", err
}
cmd := exec.Command(path, args...)
// Load OS environment
cmd.Env = os.Environ()
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
return strings.Trim(string(out), "/"), nil
}
// Generate - Configure and compile an implant
type Generate struct {
StageOptions // Command makes use of full stage options
}
// StageOptions - All these options, regrouped by area, are used by any command that needs full
// configuration information for a stage Sliver implant.
type StageOptions struct {
// CoreOptions - All options about OS/arch, files to save, debugs, etc.
CoreOptions struct {
OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"`
Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"`
Format string `long:"format" short:"f" description:"output formats (exe, shared (DLL), service (see 'psexec' for info), shellcode (Windows only)" default:"exe" value-name:"stage formats"`
Profile string `long:"profile-name" description:"implant profile name to use (use with generate-profile)"`
Name string `long:"name" short:"N" description:"implant name to use (overrides random name generation)"`
Save string `long:"save" short:"s" description:"directory/file where to save binary"`
Debug bool `long:"debug" short:"d" description:"enable debug features (incompatible with obfuscation, and prevailing)"`
} `group:"core options"`
// TransportOptions - All options pertaining to transport/RPC matters
TransportOptions struct {
MTLS []string `long:"mtls" short:"m" description:"mTLS C2 domain(s), comma-separated (ex: mtls://host:port)" env-delim:","`
DNS []string `long:"dns" short:"n" description:"DNS C2 domain(s), comma-separated (ex: dns://mydomain.com)" env-delim:","`
HTTP []string `long:"http" short:"h" description:"HTTP(S) C2 domain(s)" env-delim:","`
NamedPipe []string `long:"named-pipe" short:"p" description:"Named pipe transport strings, comma-separated" env-delim:","`
TCPPivot []string `long:"tcp-pivot" short:"i" description:"TCP pivot transport strings, comma-separated" env-delim:","`
Reconnect int `long:"reconnect" short:"j" description:"attempt to reconnect every n second(s)" default:"60"`
MaxErrors int `long:"max-errors" short:"k" description:"max number of transport errors" default:"10"`
} `group:"transport options"`
// SecurityOptions - All security-oriented options like restrictions.
SecurityOptions struct {
LimitDatetime string `long:"limit-datetime" short:"w" description:"limit execution to before datetime"`
LimitDomain bool `long:"limit-domain-joined" short:"D" description:"limit execution to domain joined machines"`
LimitUsername string `long:"limit-username" short:"U" description:"limit execution to specified username"`
LimitHosname string `long:"limit-hostname" short:"H" description:"limit execution to specified hostname"`
LimitFileExits string `long:"limit-file-exists" short:"F" description:"limit execution to hosts with this file in the filesystem"`
} `group:"security options"`
// EvasionOptions - All proactive security options (obfuscation, evasion, etc)
EvasionOptions struct {
Canary []string `long:"canary" short:"c" description:"DNS canary domain strings, comma-separated" env-delim:","`
SkipSymbols bool `long:"skip-obfuscation" short:"b" description:"skip binary/symbol obfuscation"`
Evasion bool `long:"evasion" short:"e" description:"enable evasion features"`
} `group:"evasion options"`
}
// Execute - Configure and compile an implant
func (g *Generate) Execute(args []string) (err error) {
save := g.CoreOptions.Save
if save == "" {
save, _ = os.Getwd()
}
fmt.Println("Executed 'generate' command. ")
return
}
// Regenerate - Recompile an implant by name, passed as argument (completed)
type Regenerate struct {
Positional struct {
ImplantName string `description:"Name of Sliver implant to recompile" required:"1-1"`
} `positional-args:"yes" required:"yes"`
Save string `long:"save" short:"s" description:"Directory/file where to save binary"`
}
// Execute - Recompile an implant with a given profile
func (r *Regenerate) Execute(args []string) (err error) {
fmt.Println("Executed 'regenerate' command. ")
return
}
// GenerateStager - Generate a stager payload using MSFVenom
type GenerateStager struct {
PayloadOptions struct {
OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"`
Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"`
Format string `long:"msf-format" short:"f" description:"output format (MSF Venom formats). List is auto-completed" default:"raw" value-name:"MSF Venom transform formats"`
BadChars string `long:"badchars" short:"b" description:"bytes to exclude from stage shellcode"`
Save string `long:"save" short:"s" description:"directory to save the generated stager to"`
} `group:"payload options"`
TransportOptions struct {
LHost string `long:"lhost" short:"l" description:"listening host address" required:"true"`
LPort int `long:"lport" short:"p" description:"listening host port" default:"8443"`
Protocol string `long:"protocol" short:"P" description:"staging protocol (tcp/http/https)" default:"tcp" value-name:"stager protocol"`
} `group:"transport options"`
}
// Execute - Generate a stager payload using MSFVenom
func (g *GenerateStager) Execute(args []string) (err error) {
fmt.Println("Executed 'generate stager' subcommand. ")
return
}
// Log - Log management commands. Sets log level by default.
type Log struct {
Positional struct {
Level string `description:"log level to filter by" required:"1-1"`
Components []string `description:"components on which to apply log filter" required:"1"`
} `positional-args:"yes" required:"true"`
}
// Execute - Set the log level of one or more components
func (l *Log) Execute(args []string) (err error) {
fmt.Println("Executed 'log' command. ")
return
}
var (
Info = fmt.Sprintf("%s[-]%s ", readline.BLUE, readline.RESET)
Warn = fmt.Sprintf("%s[!]%s ", readline.YELLOW, readline.RESET)
Error = fmt.Sprintf("%s[!]%s ", readline.RED, readline.RESET)
Success = fmt.Sprintf("%s[*]%s ", readline.GREEN, readline.RESET)
Infof = fmt.Sprintf("%s[-] ", readline.BLUE) // Infof - formatted
Warnf = fmt.Sprintf("%s[!] ", readline.YELLOW) // Warnf - formatted
Errorf = fmt.Sprintf("%s[!] ", readline.RED) // Errorf - formatted
Sucessf = fmt.Sprintf("%s[*] ", readline.GREEN) // Sucessf - formatted
RPCError = fmt.Sprintf("%s[RPC Error]%s ", readline.RED, readline.RESET)
CommandError = fmt.Sprintf("%s[Command Error]%s ", readline.RED, readline.RESET)
ParserError = fmt.Sprintf("%s[Parser Error]%s ", readline.RED, readline.RESET)
DBError = fmt.Sprintf("%s[DB Error]%s ", readline.RED, readline.RESET)
)
// expand will expand a path with ~ to the $HOME of the current user.
func expand(path string) (string, error) {
if path == "" {
return path, nil
}
home := os.Getenv("HOME")
if home == "" {
usr, err := user.Current()
if err != nil {
return "", err
}
home = usr.HomeDir
}
return filepath.Abs(strings.Replace(path, "~", home, 1))
}

171
readline/examples/main.go Normal file
View File

@ -0,0 +1,171 @@
package main
import (
"fmt"
"strings"
"github.com/jessevdk/go-flags"
"github.com/maxlandon/readline"
"github.com/maxlandon/readline/completers"
)
// This file shows a typical way of using readline in a loop.
func main() {
// Instantiate a console object
console := newConsole()
// Bind commands to the console
bindCommands()
// Setup the console completers, prompts, and input modes
console.setup()
// Start the readline loop (blocking)
console.Start()
}
// newConsole - Instantiates a new console with some default behavior.
// We modify/add elements of behavior later in setup.
func newConsole() *console {
console := &console{
shell: readline.NewInstance(),
parser: commandParser,
}
return console
}
// console - A simple console example.
type console struct {
shell *readline.Instance
parser *flags.Parser
}
// setup - The console sets up various elements such as the completion system, hints,
// syntax highlighting, prompt system, commands binding, and client environment loading.
func (c *console) setup() (err error) {
// Input mode & defails
c.shell.InputMode = readline.Vim // Could be readline.Emacs for emacs input mode.
c.shell.ShowVimMode = true
c.shell.VimModeColorize = true
// Prompt: we want a two-line prompt, with a custom indicator after the Vim status
c.shell.SetPrompt("readline ")
c.shell.Multiline = true
c.shell.MultilinePrompt = " > "
// Instantiate a default completer associated with the parser
// declared in commands.go, and embedded into the console struct.
// The error is muted, because we don't pass an nil parser, therefore no problems.
defaultCompleter, _ := completers.NewCommandCompleter(c.parser)
// Register the completer for command/option completions, hints and syntax highlighting.
// The completer can handle all of them.
c.shell.TabCompleter = defaultCompleter.TabCompleter
c.shell.HintText = defaultCompleter.HintCompleter
c.shell.SyntaxHighlighter = defaultCompleter.SyntaxHighlighter
// History: by default the history is in-memory, use it with Ctrl-R
return
}
// Start - The console has a working RPC connection: we setup all
// things pertaining to the console itself, and start the input loop.
func (c *console) Start() (err error) {
// Setup console elements
err = c.setup()
if err != nil {
return fmt.Errorf("Console setup failed: %s", err)
}
// Start input loop
for {
// Read input line
line, _ := c.Readline()
// Split and sanitize input
sanitized, empty := sanitizeInput(line)
if empty {
continue
}
// Process various tokens on input (environment variables, paths, etc.)
// These tokens will be expaneded by completers anyway, so this is not absolutely required.
envParsed, _ := completers.ParseEnvironmentVariables(sanitized)
// Other types of tokens, needed by commands who expect a certain type
// of arguments, such as paths with spaces.
tokenParsed := c.parseTokens(envParsed)
// Execute the command and print any errors
if _, parserErr := c.parser.ParseArgs(tokenParsed); parserErr != nil {
fmt.Println(readline.RED + "[Error] " + readline.RESET + parserErr.Error() + "\n")
}
}
}
// Readline - Add an empty line between input line and command output.
func (c *console) Readline() (line string, err error) {
line, err = c.shell.Readline()
fmt.Println()
return
}
// sanitizeInput - Trims spaces and other unwished elements from the input line.
func sanitizeInput(line string) (sanitized []string, empty bool) {
// Assume the input is not empty
empty = false
// Trim border spaces
trimmed := strings.TrimSpace(line)
if len(line) < 1 {
empty = true
return
}
unfiltered := strings.Split(trimmed, " ")
// Catch any eventual empty items
for _, arg := range unfiltered {
if arg != "" {
sanitized = append(sanitized, arg)
}
}
return
}
// parseTokens - Parse and process any special tokens that are not treated by environment-like parsers.
func (c *console) parseTokens(sanitized []string) (parsed []string) {
// PATH SPACE TOKENS
// Catch \ tokens, which have been introduced in paths where some directories have spaces in name.
// For each of these splits, we concatenate them with the next string.
// This will also inspect commands/options/arguments, but there is no reason why a backlash should be present in them.
var pathAdjusted []string
var roll bool
var arg string
for i := range sanitized {
if strings.HasSuffix(sanitized[i], "\\") {
// If we find a suffix, replace with a space. Go on with next input
arg += strings.TrimSuffix(sanitized[i], "\\") + " "
roll = true
} else if roll {
// No suffix but part of previous input. Add it and go on.
arg += sanitized[i]
pathAdjusted = append(pathAdjusted, arg)
arg = ""
roll = false
} else {
// Default, we add our path and go on.
pathAdjusted = append(pathAdjusted, sanitized[i])
}
}
parsed = pathAdjusted
// Add new function here, act on parsed []string from now on, not sanitized
return
}

7
readline/godoc.go Normal file
View File

@ -0,0 +1,7 @@
// Package readline is a pure-Go re-imagining of the UNIX readline API
//
// This package is designed to be run independently from murex and at some
// point it will be separated into it's own git repository (at a stage when I
// am confident that murex will no longer be the primary driver for features,
// bugs or other code changes)
package readline

View File

@ -5,13 +5,13 @@ import "regexp"
// 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.
/*
func (rl *Readline) SetHintText(s string) {
func (rl *Instance) SetHintText(s string) {
rl.hintText = []rune(s)
rl.renderHelpers()
}
*/
func (rl *Readline) getHintText() {
func (rl *Instance) getHintText() {
if !rl.modeAutoFind && !rl.modeTabFind {
// Return if no hints provided by the user/engine
@ -27,7 +27,7 @@ func (rl *Readline) getHintText() {
}
// writeHintText - only writes the hint text and computes its offsets.
func (rl *Readline) writeHintText() {
func (rl *Instance) writeHintText() {
if len(rl.hintText) == 0 {
//rl.hintY = 0
return
@ -43,7 +43,7 @@ func (rl *Readline) writeHintText() {
wrapped, hintLen := WrapText(string(rl.hintText), width)
offset += hintLen
// rl.hintY = offset
// rl.hintY = offset
hintText := string(wrapped)
@ -52,14 +52,7 @@ func (rl *Readline) writeHintText() {
}
}
func (rl *Readline) resetHintText() {
func (rl *Instance) resetHintText() {
//rl.hintY = 0
rl.hintText = []rune{}
}
func (rl *Readline) insertHintText() {
if len(rl.hintText) != 0 {
// fill in hint text
rl.insert(rl.hintText)
}
}

View File

@ -29,24 +29,24 @@ type History interface {
}
// SetHistoryCtrlR - Set the history source triggered with Ctrl-r combination
func (rl *Readline) SetHistoryCtrlR(name string, history History) {
func (rl *Instance) SetHistoryCtrlR(name string, history History) {
rl.mainHistName = name
rl.mainHistory = history
}
// GetHistoryCtrlR - Returns the history source triggered by Ctrl-r
func (rl *Readline) GetHistoryCtrlR() History {
func (rl *Instance) GetHistoryCtrlR() History {
return rl.mainHistory
}
// SetHistoryAltR - Set the history source triggered with Alt-r combination
func (rl *Readline) SetHistoryAltR(name string, history History) {
func (rl *Instance) SetHistoryAltR(name string, history History) {
rl.altHistName = name
rl.altHistory = history
}
// GetHistoryAltR - Returns the history source triggered by Alt-r
func (rl *Readline) GetHistoryAltR() History {
func (rl *Instance) GetHistoryAltR() History {
return rl.altHistory
}
@ -101,7 +101,7 @@ func (h *NullHistory) Dump() interface{} {
}
// Browse historic lines:
func (rl *Readline) walkHistory(i int) {
func (rl *Instance) walkHistory(i int) {
var (
old, new string
dedup bool
@ -123,7 +123,7 @@ func (rl *Readline) walkHistory(i int) {
// When we are exiting the current line buffer to move around
// the history, we make buffer the current line
if rl.histOffset == 0 && rl.histOffset+i == 1 {
if rl.histOffset == 0 && rl.histOffset + i == 1 {
rl.lineBuf = string(rl.line)
}
@ -168,7 +168,7 @@ func (rl *Readline) walkHistory(i int) {
// completeHistory - Populates a CompletionGroup with history and returns it the shell
// we populate only one group, so as to pass it to the main completion engine.
func (rl *Readline) completeHistory() (hist []*CompletionGroup) {
func (rl *Instance) completeHistory() (hist []*CompletionGroup) {
hist = make([]*CompletionGroup, 1)
hist[0] = &CompletionGroup{

View File

@ -4,12 +4,12 @@ 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 *Readline) SetInfoText(s string) {
func (rl *Instance) SetInfoText(s string) {
rl.infoText = []rune(s)
rl.renderHelpers()
}
func (rl *Readline) getInfoText() {
func (rl *Instance) getInfoText() {
if !rl.modeAutoFind && !rl.modeTabFind {
// Return if no infos provided by the user/engine
@ -25,7 +25,7 @@ func (rl *Readline) getInfoText() {
}
// writeInfoText - only writes the info text and computes its offsets.
func (rl *Readline) writeInfoText() {
func (rl *Instance) writeInfoText() {
if len(rl.infoText) == 0 {
rl.infoY = 0
return
@ -50,7 +50,7 @@ func (rl *Readline) writeInfoText() {
}
}
func (rl *Readline) resetInfoText() {
func (rl *Instance) resetInfoText() {
rl.infoY = 0
rl.infoText = []rune{}
}

View File

@ -5,16 +5,12 @@ import (
"os"
"regexp"
"sync"
"github.com/arnodel/golua/lib/packagelib"
)
// Instance is used to encapsulate the parameter group and run time of any given
// readline instance so that you can reuse the readline API for multiple entry
// captures without having to repeatedly unload configuration.
// #type
type Readline struct {
type Instance struct {
//
// Input Modes -------------------------------------------------------------------------------
@ -35,13 +31,13 @@ type Readline struct {
Multiline bool // If set to true, the shell will have a two-line prompt.
MultilinePrompt string // If multiline is true, this is the content of the 2nd line.
mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt
rightPrompt string
rightPromptLen int
realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line.
defaultPrompt []rune
promptLen int
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs
mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt
rightPrompt string
rightPromptLen int
realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line.
defaultPrompt []rune
promptLen int
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs
//
// Input Line ---------------------------------------------------------------------------------
@ -118,9 +114,9 @@ type Readline struct {
searchMode FindMode // Used for varying hints, and underlying functions called
regexSearch *regexp.Regexp // Holds the current search regex match
search string
mainHist bool // Which history stdin do we want
histInfo []rune // We store a piece of hist info, for dual history sources
Searcher func(string, []string) []string
mainHist bool // Which history stdin do we want
histInfo []rune // We store a piece of hist info, for dual history sources
Searcher func(string, []string) []string
//
// History -----------------------------------------------------------------------------------
@ -204,19 +200,17 @@ type Readline struct {
// concurency
mutex sync.Mutex
ViModeCallback func(ViMode)
ViModeCallback func(ViMode)
ViActionCallback func(ViAction, []string)
RawInputCallback func([]rune) // called on all input
bufferedOut *bufio.Writer
Loader packagelib.Loader
}
// NewInstance is used to create a readline instance and initialise it with sane defaults.
func NewInstance() *Readline {
rl := new(Readline)
func NewInstance() *Instance {
rl := new(Instance)
// Prompt
rl.Multiline = false
@ -246,14 +240,12 @@ func NewInstance() *Readline {
var err error
rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine))
if err != nil {
//rl.RefreshPromptLog(err.Error())
rl.RefreshPromptLog(err.Error())
rl.infoText = []rune(Red("Failed to match search regexp"))
}
for _, hay := range haystack {
if rl.regexSearch == nil {
continue
}
if rl.regexSearch == nil { continue }
if rl.regexSearch.MatchString(hay) {
suggs = append(suggs, hay)
}
@ -264,11 +256,6 @@ func NewInstance() *Readline {
rl.bufferedOut = bufio.NewWriter(os.Stdout)
rl.Loader = packagelib.Loader{
Name: "readline",
Load: rl.luaLoader,
}
// Registers
rl.initRegisters()

View File

@ -6,7 +6,7 @@ import (
// When the DelayedSyntaxWorker gives us a new line, we need to check if there
// is any processing to be made, that all lines match in terms of content.
func (rl *Readline) updateLine(line []rune) {
func (rl *Instance) updateLine(line []rune) {
if len(rl.currentComp) > 0 {
} else {
@ -18,7 +18,7 @@ func (rl *Readline) updateLine(line []rune) {
// getLine - In many places we need the current line input. We either return the real line,
// or the one that includes the current completion candidate, if there is any.
func (rl *Readline) GetLine() []rune {
func (rl *Instance) GetLine() []rune {
if len(rl.currentComp) > 0 {
return rl.lineComp
}
@ -30,7 +30,7 @@ func (rl *Readline) GetLine() []rune {
// function is only ever called once, and after having moved back to prompt position
// and having printed the line: this is so that at any moment, everyone has the good
// values for moving around, synchronized with the update input line.
func (rl *Readline) echo() {
func (rl *Instance) echo() {
// Then we print the prompt, and the line,
hideCursor()
@ -79,7 +79,7 @@ func (rl *Readline) echo() {
unhideCursor()
}
func (rl *Readline) insert(r []rune) {
func (rl *Instance) insert(r []rune) {
for {
// I don't really understand why `0` is creaping in at the end of the
// array but it only happens with unicode characters.
@ -112,11 +112,11 @@ func (rl *Readline) insert(r []rune) {
rl.updateHelpers()
}
func (rl *Readline) Insert(t string) {
func (rl *Instance) Insert(t string) {
rl.insert([]rune(t))
}
func (rl *Readline) deleteX() {
func (rl *Instance) deleteX() {
switch {
case len(rl.line) == 0:
return
@ -134,7 +134,7 @@ func (rl *Readline) deleteX() {
rl.updateHelpers()
}
func (rl *Readline) deleteBackspace(forward bool) {
func (rl *Instance) deleteBackspace(forward bool) {
switch {
case len(rl.line) == 0:
return
@ -153,7 +153,7 @@ func (rl *Readline) deleteBackspace(forward bool) {
rl.updateHelpers()
}
func (rl *Readline) clearLine() {
func (rl *Instance) clearLine() {
if len(rl.line) == 0 {
return
}
@ -179,21 +179,21 @@ func (rl *Readline) clearLine() {
rl.clearVirtualComp()
}
func (rl *Readline) deleteToBeginning() {
func (rl *Instance) deleteToBeginning() {
rl.resetVirtualComp(false)
// Keep the line length up until the cursor
rl.line = rl.line[rl.pos:]
rl.pos = 0
}
func (rl *Readline) deleteToEnd() {
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 *Readline) emacsForwardWord(tokeniser tokeniser) (adjust int) {
func (rl *Instance) emacsForwardWord(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return
@ -214,7 +214,7 @@ func (rl *Readline) emacsForwardWord(tokeniser tokeniser) (adjust int) {
return
}
func (rl *Readline) emacsBackwardWord(tokeniser tokeniser) (adjust int) {
func (rl *Instance) emacsBackwardWord(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return

View File

@ -8,20 +8,20 @@ import (
// SetPrompt will define the readline prompt string.
// It also calculates the runes in the string as well as any non-printable escape codes.
func (rl *Readline) SetPrompt(s string) {
func (rl *Instance) SetPrompt(s string) {
rl.mainPrompt = s
rl.computePrompt()
}
// SetRightPrompt sets the right prompt.
func (rl *Readline) SetRightPrompt(s string) {
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,
// an asynchronous event) without bothering the user, and by "pushing" the prompt below the message.
func (rl *Readline) RefreshPromptLog(log string) (err error) {
func (rl *Instance) RefreshPromptLog(log string) (err error) {
// We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion {
@ -73,7 +73,7 @@ func (rl *Readline) RefreshPromptLog(log string) (err error) {
}
// RefreshPromptInPlace - Refreshes the prompt in the very same place he is.
func (rl *Readline) 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.
// Prompt data intependent
if !rl.modeTabCompletion {
@ -117,7 +117,7 @@ func (rl *Readline) RefreshPromptInPlace(prompt string) (err error) {
// @prompt => If not nil (""), will use this prompt instead of the currently set prompt.
// @offset => Used to set the number of lines to go upward, before reprinting. Set to 0 if not used.
// @clearLine => If true, will clean the current input line on the next refresh.
func (rl *Readline) RefreshPromptCustom(prompt string, offset int, clearLine bool) (err error) {
func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine bool) (err error) {
// We adjust cursor movement, depending on which mode we're currently in.
if !rl.modeTabCompletion {
@ -166,7 +166,7 @@ func (rl *Readline) RefreshPromptCustom(prompt string, offset int, clearLine boo
// computePrompt - At any moment, returns an (1st or 2nd line) actualized prompt,
// considering all input mode parameters and prompt string values.
func (rl *Readline) computePrompt() (prompt []rune) {
func (rl *Instance) computePrompt() (prompt []rune) {
if rl.Multiline {
if rl.MultilinePrompt != "" {
rl.realPrompt = []rune(rl.MultilinePrompt)
@ -194,7 +194,7 @@ func (rl *Readline) computePrompt() (prompt []rune) {
return
}
func (rl *Readline) colorizeVimPrompt(p []rune) (cp []rune) {
func (rl *Instance) colorizeVimPrompt(p []rune) (cp []rune) {
if rl.VimModeColorize {
return []rune(fmt.Sprintf("%s%s%s", BOLD, string(p), RESET))
}
@ -211,8 +211,8 @@ func getRealLength(s string) (l int) {
return getWidth([]rune(stripped))
}
func (rl *Readline) echoRightPrompt() {
if rl.fullX < GetTermWidth()-rl.rightPromptLen-1 {
func (rl *Instance) echoRightPrompt() {
if rl.fullX < GetTermWidth() - rl.rightPromptLen - 1 {
moveCursorForwards(GetTermWidth())
moveCursorBackwards(rl.rightPromptLen)
print(rl.rightPrompt)

201
readline/raw/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Some files were not shown because too many files have changed in this diff Show More