2
2
mirror of https://github.com/Hilbis/Hilbish synced 2025-07-04 10:12:03 +00:00

Compare commits

..

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

151 changed files with 4329 additions and 4037 deletions

View File

@ -1,12 +1,8 @@
name: Build
on:
push:
branches:
- master
pull_request:
branches:
- master
- push
- pull_request
jobs:
build:
@ -23,18 +19,18 @@ jobs:
goos: windows
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
submodules: true
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v2
with:
go-version: '1.22.2'
go-version: '1.18.8'
- name: Download Task
run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d'
- name: Build
run: GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} ./bin/task
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v2
if: matrix.goos == 'windows'
with:
name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }}
@ -48,7 +44,7 @@ jobs:
libs
docs
emmyLuaDocs
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v2
if: matrix.goos != 'windows'
with:
name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }}

View File

@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

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/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)
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
- 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

@ -9,7 +9,7 @@ jobs:
create-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- uses: taiki-e/create-gh-release-action@v1
with:
title: Hilbish $tag
@ -30,7 +30,7 @@ jobs:
- goarch: arm64
goos: windows
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 0

View File

@ -1,34 +1,27 @@
name: Build website
on:
push:
branches:
- master
tags:
- v[0-9]+.*
pull_request:
branches:
- master
workflow_dispatch:
- push
- pull_request
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.111.3'
hugo-version: 'latest'
extended: true
- name: Set branch name
id: branch
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/*/}}" >> "$GITHUB_ENV"
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_ENV"
- name: Fix base URL
if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea'
@ -39,14 +32,14 @@ jobs:
- name: Deploy
if: env.BRANCH_NAME == 'master' && github.repository_owner == 'Rosettea'
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./website/public
keep_files: true
- name: Deploy
if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea'
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./website/public

View File

@ -1,111 +1,5 @@
# 🎀 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.
Displayed tips can be modified via the `hilbish.tips` table.
### Fixed
- Fix a minor regression related to the cd command not working with relative paths
- Updated the motd for 2.3
## [2.3.0] - 2024-07-20
### Added
- `commander.registry` function to get all registered commanders.
- `fs.pipe` function to get a pair of connected files (a pipe).
- Added an alternative 2nd parameter to `hilbish.run`, which is `streams`.
`streams` is a table of input and output streams to run the command with.
It uses these 3 keys:
- `input` as standard input for the command
- `out` as standard output
- `err` as standard error
Here is a minimal example of the new usage which allows users to now pipe commands
directly via Lua functions:
```lua
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
})
```
### Changed
- The `-S` flag will be set to Hilbish's absolute path
- Hilbish now builds on any Unix (if any dependencies also work, which should.)
### Fixed
- Fix ansi attributes causing issues with text when cut off in greenhouse
- Fix greenhouse appearing on terminal resize
- Fix crashes when history goes out of bounds when using history navigation
- `exec` command should return if no arg presented
- Commanders can now be cancelled by Ctrl-C and wont hang the shell anymore.
See [issue 198](https://github.com/Rosettea/Hilbish/issues/198).
- Shell interpreter can now preserve its environment and set PWD properly.
## [2.2.3] - 2024-04-27
### Fixed
- Highligher and hinter work now, since it was regressed from the previous minor release.
- `cat` command no longer prints extra newline at end of each file
### Added
- `cat` command now reads files in chunks, allowing for reading large files
## [2.2.2] - 2024-04-16
### Fixed
- Line refresh fixes (less flicker)
@ -814,12 +708,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
[2.2.2]: https://github.com/Rosettea/Hilbish/compare/v2.2.1...v2.2.2
[2.2.1]: https://github.com/Rosettea/Hilbish/compare/v2.2.0...v2.2.1
[2.2.0]: https://github.com/Rosettea/Hilbish/compare/v2.1.0...v2.2.0

View File

@ -1,6 +1,3 @@
> [!TIP]
> Check out [Hilbish: Midnight Edition](https://github.com/Rosettea/Hilbish/tree/midnight-edition) if you want to use C Lua, LuaJIT or anything related!
<img src="./assets/hilbish-logo-and-text.png" width=512><br>
<blockquote>
🌓 The Moon-powered shell! A comfy and extensible shell for Lua fans! 🌺 ✨
@ -13,23 +10,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
@ -43,7 +36,7 @@ on the website for distributed binaries from GitHub or other package repositorie
Otherwise, continue reading for steps on compiling.
## Prerequisites
- [Go 1.22+](https://go.dev)
- [Go 1.17+](https://go.dev)
- [Task](https://taskfile.dev/installation/) (**Go on the hyperlink here to see Task's install method for your OS.**)
## Build

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}}'

209
api.go
View File

@ -13,7 +13,7 @@
package main
import (
//"bytes"
"bytes"
"errors"
"fmt"
"os"
@ -25,31 +25,31 @@ 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/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 +62,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 +102,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 +119,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 +132,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,36 +152,50 @@ 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 {
return errors.New("expected metatable argument")
// run(cmd, returnOut) -> exitCode (number), stdout (string), stderr (string)
// Runs `cmd` in Hilbish's shell script interpreter.
// #param cmd string
// #param returnOut boolean If this is true, the function will return the standard output and error of the command instead of printing it.
// #returns number, string, string
func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
cmd, err := c.StringArg(0)
if err != nil {
return nil, err
}
val := ud.Value()
var varstrm io.Writer
if f, ok := val.(*iolib.File); ok {
varstrm = f.Handle()
}
if f, ok := val.(*sink); ok {
varstrm = f.writer
}
if varstrm == nil {
return errors.New("expected either a sink or file")
}
if errStream {
strms.stderr = varstrm
var terminalOut bool
if len(c.Etc()) != 0 {
tout := c.Etc()[0]
termOut, ok := tout.TryBool()
terminalOut = termOut
if !ok {
return nil, errors.New("bad argument to run (expected boolean, got " + tout.TypeName() + ")")
}
} else {
strms.stdout = varstrm
terminalOut = true
}
return nil
var exitcode uint8
stdout, stderr, err := execCommand(cmd, terminalOut)
if code, ok := interp.IsExitStatus(err); ok {
exitcode = code
} else if err != nil {
exitcode = 1
}
stdoutStr := ""
stderrStr := ""
if !terminalOut {
stdoutStr = stdout.(*bytes.Buffer).String()
stderrStr = stderr.(*bytes.Buffer).String()
}
return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil
}
*/
// cwd() -> string
// Returns the current directory of the shell.
@ -193,6 +206,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.
@ -210,7 +224,7 @@ func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// substitute with an empty string
prompt = ""
}
lualr := &lineReader{
rl: readline.NewInstance(),
}
@ -263,13 +277,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
@ -290,7 +302,7 @@ will look like:
user ~ echo "hey
--> ...!"
so then you get
so then you get
user ~ echo "hey
--> ...!"
hey ...!
@ -300,7 +312,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 +398,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 +416,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
@ -480,7 +492,7 @@ func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
interval := time.Duration(ms) * time.Millisecond
timer := timers.create(timerTimeout, interval, cb)
timer.start()
return c.PushingNext1(t.Runtime, rt.UserDataValue(timer.ud)), nil
}
@ -571,7 +583,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
@ -597,12 +609,12 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
cmd := strings.Split(alias, " ")[0]
// check for commander
if cmds.Commands[cmd] != nil {
if commands[cmd] != nil {
// they dont resolve to a path, so just send the cmd
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 +637,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,21 +707,10 @@ 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
func hlhighlighter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
line, err := c.StringArg(0)
if err != nil {
return nil, err
}
return c.PushingNext1(t.Runtime, rt.StringValue(line)), nil
return c.Next(), nil
}

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,24 +274,24 @@ 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
}
em.DocPiece = dps
emmyDocs[mod] = append(emmyDocs[mod], em)
return dps
}
@ -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,12 +487,8 @@ provided by Hilbish.
continue
}
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, 0, fmt.Sprintf(`<a href="#%s">%s</a>`, dps.FuncName, dps.FuncSig))
mdTable.SetContent(i - diff, 1, dps.Doc[0])
}
f.WriteString(mdTable.String())
f.WriteString("\n")
@ -543,6 +501,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 +516,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 +534,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 +621,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,83 +81,30 @@ 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([[
f:write(string.format([[
<h4 class='heading'>
%s
<a href="#%s" class='heading-link'>
@ -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
}
@ -128,7 +128,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
}
// add lua registered commands to completions
for cmdName := range cmds.Commands {
for cmdName := range commands {
if strings.HasPrefix(cmdName, query) {
completions = append(completions, cmdName)
}
@ -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

@ -26,11 +26,8 @@ In this example, a command with the name of `hello` is created
that will print `Hello world!` to output. One question you may
have is: What is the `sinks` parameter?
The `sinks` parameter is a table with 3 keys: `input`, `out`, and `err`.
There is an `in` alias to `input`, but it requires using the string accessor syntax (`sinks['in']`)
as `in` is also a Lua keyword, so `input` is preferred for use.
All of them are a <a href="/Hilbish/docs/api/hilbish/#sink" style="text-decoration: none;">Sink</a>.
In the future, `sinks.in` will be removed.
The `sinks` parameter is a table with 3 keys: `in`, `out`,
and `err`. All of them are a <a href="/Hilbish/docs/api/hilbish/#sink" style="text-decoration: none;">Sink</a>.
- `in` is the standard input.
You may use the read functions on this sink to get input from the user.
@ -44,7 +41,6 @@ This sink is for writing errors, as the name would suggest.
|----|----|
|<a href="#deregister">deregister(name)</a>|Removes the named command. Note that this will only remove Commander-registered commands.|
|<a href="#register">register(name, cb)</a>|Adds a new command with the given `name`. When Hilbish has to run a command with a name,|
|<a href="#registry">registry() -> table</a>|Returns all registered commanders. Returns a list of tables with the following keys:|
<hr>
<div id='deregister'>
@ -95,19 +91,3 @@ end)
```
</div>
<hr>
<div id='registry'>
<h4 class='heading'>
commander.registry() -> table
<a href="#registry" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns all registered commanders. Returns a list of tables with the following keys:
- `exec`: The function used to run the commander. Commanders require args and sinks to be passed.
#### Parameters
This function has no parameters.
</div>

View File

@ -23,7 +23,6 @@ library offers more functions and will work on any operating system Hilbish does
|<a href="#glob">glob(pattern) -> matches (table)</a>|Match all files based on the provided `pattern`.|
|<a href="#join">join(...path) -> string</a>|Takes any list of paths and joins them based on the operating system's path separator.|
|<a href="#mkdir">mkdir(name, recursive)</a>|Creates a new directory with the provided `name`.|
|<a href="#pipe">fpipe() -> File, File</a>|Returns a pair of connected files, also known as a pipe.|
|<a href="#readdir">readdir(path) -> table[string]</a>|Returns a list of all files and directories in the provided path.|
|<a href="#stat">stat(path) -> {}</a>|Returns the information about a given `path`.|
@ -184,22 +183,6 @@ fs.mkdir('./foo/bar', true)
```
</div>
<hr>
<div id='pipe'>
<h4 class='heading'>
fs.fpipe() -> File, File
<a href="#pipe" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns a pair of connected files, also known as a pipe.
The type returned is a Lua file, same as returned from `io` functions.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='readdir'>
<h4 class='heading'>

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, returnOut) -> 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,49 @@ 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, returnOut) -> 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.
#### Parameters
`string` **`cmd`**
`boolean` **`returnOut`**
If this is true, the function will return the standard output and error of the command instead of printing it.
</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 +496,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 +519,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.
Runs a command in Hilbish's shell script interpreter.
This is the equivalent of using `source`.
#### Parameters
`name` **`string`**
Name of the runner
`runner` **`function|table`**
</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?`**
`string` **`cmd`**
</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

@ -18,7 +18,7 @@ In other usage, you may want to use a format string instead of having
multiple nested functions for different styles. This is where the format
function comes in. You can used named keywords to style a section of text.
The list of arguments are:
The list of arguments are:
Colors:
- black
- red
@ -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,92 +1,14 @@
---
title: Module dirs
description: internal directory management
description: No description.
layout: doc
menu:
docs:
parent: "Nature"
---
## Introduction
The dirs module defines a small set of functions to store and manage
directories.
## 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'>
dirs.peak(num)
<a href="#peak" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</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>
<div id='pop'>
<h4 class='heading'>
dirs.pop(num)
<a href="#pop" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
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>
<div id='recent'>
<h4 class='heading'>
dirs.recent(idx)
<a href="#recent" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Get entry from recent directories list based on index.
#### Parameters
`idx` **`number`**
</div>
<hr>
<div id='setOld'>
<div id='setOld'>
<h4 class='heading'>
dirs.setOld(d)
<a href="#setOld" class='heading-link'>
@ -96,8 +18,62 @@ dirs.setOld(d)
Sets the old directory string.
#### Parameters
`d` **`string`**
`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>
<hr>
<div id='peak'>
<h4 class='heading'>
dirs.peak(num)
<a href="#peak" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Look at `num` amount of recent directories, starting from the latest.
#### Parameters
`num` **`number`**
</div>
<hr>
<div id='pop'>
<h4 class='heading'>
dirs.pop(num)
<a href="#pop" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Remove the specified amount of dirs from the recent directories list.
#### Parameters
`num` **`number`**
</div>
<hr>
<div id='recent'>
<h4 class='heading'>
dirs.recent(idx)
<a href="#recent" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Get entry from recent directories list based on index.
#### Parameters
`idx` **`number`**
</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

@ -11,8 +11,4 @@ function commander.deregister(name) end
---
function commander.register(name, cb) end
--- Returns all registered commanders. Returns a list of tables with the following keys:
--- - `exec`: The function used to run the commander. Commanders require args and sinks to be passed.
function commander.registry() end
return commander

View File

@ -34,10 +34,6 @@ function fs.join(...path) end
---
function fs.mkdir(name, recursive) end
--- Returns a pair of connected files, also known as a pipe.
--- The type returned is a Lua file, same as returned from `io` functions.
function fs.fpipe() end
--- Returns a list of all files and directories in the provided path.
function fs.readdir(path) end

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,18 @@ 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.
function hilbish.run(cmd, returnOut) 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 +162,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 +194,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 +256,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

507
exec.go
View File

@ -1,26 +1,201 @@
package main
import (
"bytes"
"context"
"errors"
"os/exec"
"fmt"
"io"
"os"
"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 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 +204,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 +225,301 @@ 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, true)
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, terminalOut bool) (io.Writer, io.Writer, error) {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return nil, nil, err
}
runner, _ := interp.New()
var stdout io.Writer
var stderr io.Writer
if terminalOut {
interp.StdIO(os.Stdin, os.Stdout, os.Stderr)(runner)
} else {
stdout = new(bytes.Buffer)
stderr = new(bytes.Buffer)
interp.StdIO(os.Stdin, stdout, stderr)(runner)
}
buf := new(bytes.Buffer)
printer := syntax.NewPrinter()
var bg bool
for _, stmt := range file.Stmts {
bg = false
if stmt.Background {
bg = true
printer.Print(buf, stmt.Cmd)
stmtStr := buf.String()
buf.Reset()
jobs.add(stmtStr, []string{}, "")
}
interp.ExecHandler(execHandle(bg))(runner)
err = runner.Run(context.TODO(), stmt)
if err != nil {
return stdout, stderr, err
}
}
return stdout, stderr, nil
}
func execHandle(bg bool) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " "))
// 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 commands[args[0]] != 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("out"), rt.UserDataValue(stdout.ud))
sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud))
luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
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(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 +535,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 +562,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
// +build linux darwin
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
// +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
}
}

33
go.mod
View File

@ -1,37 +1,34 @@
module hilbish
go 1.21
toolchain go1.22.2
go 1.18
require (
github.com/arnodel/golua v0.0.0-20230215163904-e0b5347eaaa1
github.com/arnodel/golua v0.0.0-20220221163911-dfcf252b6f86
github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504
github.com/blackfireio/osinfo v1.0.5
github.com/maxlandon/readline v1.0.14
github.com/blackfireio/osinfo v1.0.3
github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036
github.com/pborman/getopt v1.1.0
github.com/sahilm/fuzzy v0.1.1
golang.org/x/sys v0.22.0
golang.org/x/term v0.22.0
mvdan.cc/sh/v3 v3.8.0
github.com/sahilm/fuzzy v0.1.0
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
mvdan.cc/sh/v3 v3.5.1
)
require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/arnodel/strftime v0.1.6 // indirect
github.com/evilsocket/islazy v1.11.0 // indirect
github.com/evilsocket/islazy v1.10.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.14.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
golang.org/x/text v0.3.7 // 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.20220524215627-dfd9a4fa219b
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-20221213193027-cbf6d4e4d345

87
go.sum
View File

@ -1,46 +1,69 @@
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-20221213193027-cbf6d4e4d345 h1:QNYjYDogUSiNUkffbhFSrSCtpZhofeiVYGFN2FI4wSs=
github.com/Rosettea/golua v0.0.0-20221213193027-cbf6d4e4d345/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b h1:s5eDMhBk6H1BgipgLub/gv9qeyBaTuiHM0k3h2/9TSE=
github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/arnodel/edit v0.0.0-20220202110212-dfc8d7a13890/go.mod h1:AcpttpuZBaL9xl8/CX+Em4fBTUbwIkJ66RiAsJlNrBk=
github.com/arnodel/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw=
github.com/arnodel/strftime v0.1.6/go.mod h1:5NbK5XqYK8QpRZpqKNt4OlxLtIB8cotkLk4KTKzJfWs=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504 h1:R1/AOzdMbopSliUTTEHvHbyNmnZ3YxY5GvdhTkpPsSY=
github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504/go.mod h1:kHBCvAXJIatTX1pw6tLiOspjGc3MhUDRlog9yrCUS+k=
github.com/blackfireio/osinfo v1.0.5 h1:6hlaWzfcpb87gRmznVf7wSdhysGqLRz9V/xuSdCEXrA=
github.com/blackfireio/osinfo v1.0.5/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0=
github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/blackfireio/osinfo v1.0.3 h1:Yk2t2GTPjBcESv6nDSWZKO87bGMQgO+Hi9OoXPpxX8c=
github.com/blackfireio/osinfo v1.0.3/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc=
github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo=
github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw=
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0=
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0=

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

@ -17,11 +17,8 @@ In this example, a command with the name of `hello` is created
that will print `Hello world!` to output. One question you may
have is: What is the `sinks` parameter?
The `sinks` parameter is a table with 3 keys: `input`, `out`, and `err`.
There is an `in` alias to `input`, but it requires using the string accessor syntax (`sinks['in']`)
as `in` is also a Lua keyword, so `input` is preferred for use.
All of them are a @Sink.
In the future, `sinks.in` will be removed.
The `sinks` parameter is a table with 3 keys: `in`, `out`,
and `err`. All of them are a @Sink.
- `in` is the standard input.
You may use the read functions on this sink to get input from the user.
@ -43,13 +40,11 @@ import (
type Commander struct{
Events *bait.Bait
Loader packagelib.Loader
Commands map[string]*rt.Closure
}
func New(rtm *rt.Runtime) *Commander {
c := &Commander{
func New(rtm *rt.Runtime) Commander {
c := Commander{
Events: bait.New(rtm),
Commands: make(map[string]*rt.Closure),
}
c.Loader = packagelib.Loader{
Load: c.loaderFunc,
@ -63,7 +58,6 @@ func (c *Commander) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
exports := map[string]util.LuaExport{
"register": util.LuaExport{c.cregister, 2, false},
"deregister": util.LuaExport{c.cderegister, 1, false},
"registry": util.LuaExport{c.cregistry, 0, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
@ -94,7 +88,7 @@ func (c *Commander) cregister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) {
return nil, err
}
c.Commands[cmdName] = cmd
c.Events.Emit("commandRegister", cmdName, cmd)
return ct.Next(), err
}
@ -111,23 +105,7 @@ func (c *Commander) cderegister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) {
return nil, err
}
delete(c.Commands, cmdName)
c.Events.Emit("commandDeregister", cmdName)
return ct.Next(), err
}
// registry() -> table
// Returns all registered commanders. Returns a list of tables with the following keys:
// - `exec`: The function used to run the commander. Commanders require args and sinks to be passed.
// #returns table
func (c *Commander) cregistry(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) {
registryLua := rt.NewTable()
for cmdName, cmd := range c.Commands {
cmdTbl := rt.NewTable()
cmdTbl.Set(rt.StringValue("exec"), rt.FunctionValue(cmd))
registryLua.Set(rt.StringValue(cmdName), rt.TableValue(cmdTbl))
}
return ct.PushingNext1(t.Runtime, rt.TableValue(registryLua)), nil
}

View File

@ -18,7 +18,6 @@ import (
rt "github.com/arnodel/golua/runtime"
"github.com/arnodel/golua/lib/packagelib"
"github.com/arnodel/golua/lib/iolib"
)
var Loader = packagelib.Loader{
@ -37,7 +36,6 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
"dir": util.LuaExport{fdir, 1, false},
"glob": util.LuaExport{fglob, 1, false},
"join": util.LuaExport{fjoin, 0, true},
"pipe": util.LuaExport{fpipe, 0, false},
}
mod := rt.NewTable()
util.SetExports(rtm, mod, exports)
@ -96,23 +94,12 @@ 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
}
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))
return c.Next(), err
}
@ -239,22 +226,6 @@ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
return c.Next(), err
}
// fpipe() -> File, File
// Returns a pair of connected files, also known as a pipe.
// 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) {
rf, wf, err := os.Pipe()
if err != nil {
return nil, err
}
rfLua := iolib.NewFile(rf, 0)
wfLua := iolib.NewFile(wf, 0)
return c.PushingNext(t.Runtime, rfLua.Value(t.Runtime), wfLua.Value(t.Runtime)), nil
}
// readdir(path) -> table[string]
// Returns a list of all files and directories in the provided path.
// #param dir string

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())
}

View File

@ -1,4 +1,4 @@
//go:build windows
// +build windows
package main

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

@ -1,4 +1,4 @@
//go:build unix
// +build darwin linux
package main
@ -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

@ -1,16 +1,11 @@
//go:build windows
// +build windows
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")
}

113
lua.go
View File

@ -3,40 +3,79 @@ 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
lib.LoadLibs(l, fs.Loader)
lib.LoadLibs(l, terminal.Loader)
cmds := commander.New(l)
// When a command from Lua is added, register it for use
cmds.Events.On("commandRegister", func(args ...interface{}) {
cmdName := args[0].(string)
cmd := args[1].(*rt.Closure)
commands[cmdName] = cmd
})
cmds.Events.On("commandDeregister", func(args ...interface{}) {
cmdName := args[0].(string)
delete(commands, cmdName)
})
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 +84,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

64
main.go
View File

@ -6,7 +6,6 @@ import (
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
@ -15,7 +14,6 @@ import (
"hilbish/util"
"hilbish/golibs/bait"
"hilbish/golibs/commander"
rt "github.com/arnodel/golua/runtime"
"github.com/pborman/getopt"
@ -27,6 +25,7 @@ var (
l *rt.Runtime
lr *lineReader
commands = map[string]*rt.Closure{}
luaCompletions = map[string]*rt.Closure{}
confDir string
@ -34,30 +33,16 @@ var (
curuser *user.User
hooks *bait.Bait
cmds *commander.Commander
defaultConfPath string
defaultHistPath string
)
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
}
}
}
}
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 {
@ -129,13 +114,7 @@ func main() {
// Set $SHELL if the user wants to
if *setshflag {
os.Setenv("SHELL", "hilbish")
path, err := exec.LookPath("hilbish")
if err == nil {
os.Setenv("SHELL", path)
}
os.Setenv("SHELL", os.Args[0])
}
lr = newLineReader("", false)
@ -151,11 +130,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 +212,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 +237,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 +293,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

@ -9,8 +9,6 @@ commander.register('cat', function(args, sinks)
usage: cat [file]...]]
end
local chunkSize = 2^13 -- 8K buffer size
for _, fName in ipairs(args) do
local f = io.open(fName)
if f == nil then
@ -19,11 +17,7 @@ usage: cat [file]...]]
goto continue
end
while true do
local block = f:read(chunkSize)
if not block then break end
sinks.out:write(block)
end
sinks.out:writeln(f:read '*a')
::continue::
end
io.flush()

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,8 +1,5 @@
local commander = require 'commander'
commander.register('exec', function(args)
if #args == 0 then
return
end
hilbish.exec(args[1])
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))
.. string.rep(' ', longest - line:len()) .. ' ')
lines[i] = ' ' .. 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
@ -62,24 +61,17 @@ function Greenhouse:updateCurrentPage(text)
page:setText(text)
end
local ansiPatters = {
'\x1b%[%d+;%d+;%d+;%d+;%d+%w',
'\x1b%[%d+;%d+;%d+;%d+%w',
'\x1b%[%d+;%d+;%d+%w',
'\x1b%[%d+;%d+%w',
'\x1b%[%d+%w'
}
function Greenhouse:sub(str, offset, limit)
local overhead = 0
local function addOverhead(s)
overhead = overhead + string.len(s)
end
local s = str
for _, pat in ipairs(ansiPatters) do
s = s:gsub(pat, addOverhead)
end
local s = str:gsub('\x1b%[%d+;%d+;%d+;%d+;%d+%w', addOverhead)
:gsub('\x1b%[%d+;%d+;%d+;%d+%w', addOverhead)
:gsub('\x1b%[%d+;%d+;%d+%w',addOverhead)
:gsub('\x1b%[%d+;%d+%w', addOverhead)
:gsub('\x1b%[%d+%w', addOverhead)
return s:sub(offset, utf8.offset(str, limit + overhead) or limit + overhead)
--return s:sub(offset, limit + overhead)
@ -102,40 +94,14 @@ function Greenhouse:draw()
self.sink:write(ansikit.getCSI(2, 'J'))
local writer = self.sink.writeln
self.attributes = {}
for i = offset, offset + self.region.height - 1 do
local resetEnd = false
if i > #lines then break end
if i == offset + self.region.height - 1 then writer = self.sink.write end
self.sink:write(ansikit.getCSI(self.start + i - offset .. ';1', 'H'))
local line = lines[i]:gsub('{separator}', function() return self.separator:rep(self.region.width - 1) end)
for _, pat in ipairs(ansiPatters) do
line:gsub(pat, function(s)
if s == lunacolors.formatColors.reset then
self.attributes = {}
resetEnd = true
else
--resetEnd = false
--table.insert(self.attributes, s)
end
end)
end
--[[
if #self.attributes ~= 0 then
for _, attr in ipairs(self.attributes) do
--writer(self.sink, attr)
end
end
]]--
self.sink:write(lunacolors.formatColors.reset)
writer(self.sink, self:sub(line:gsub('\t', ' '), self.horizOffset, self.region.width + self.horizOffset))
if resetEnd then
self.sink:write(lunacolors.formatColors.reset)
end
writer(self.sink, self:sub(line:gsub('\t', ' '), self.horizOffset, self.region.width))
end
writer(self.sink, '\27[0m')
self:render()
@ -305,15 +271,6 @@ end
function Greenhouse:input(char)
end
local function read()
terminal.saveState()
terminal.setRaw()
local c = hilbish.editor.readChar()
terminal.restoreState()
return c
end
function Greenhouse:initUi()
local ansikit = require 'ansikit'
local bait = require 'bait'
@ -323,17 +280,14 @@ function Greenhouse:initUi()
local Page = require 'nature.greenhouse.page'
local done = false
local function sigint()
bait.catch('signal.sigint', function()
ansikit.clear()
done = true
end
end)
local function resize()
bait.catch('signal.resize', function()
self:update()
end
bait.catch('signal.sigint', sigint)
bait.catch('signal.resize', resize)
end)
ansikit.screenAlt()
ansikit.clear(true)
@ -357,10 +311,15 @@ function Greenhouse:initUi()
ansikit.showCursor()
ansikit.screenMain()
end
self = nil
bait.release('signal.sigint', sigint)
bait.release('signal.resize', resize)
function read()
terminal.saveState()
terminal.setRaw()
local c = hilbish.editor.readChar()
terminal.restoreState()
return c
end
return Greenhouse

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,30 +44,24 @@ 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
if msg then
M._messages[idx].read = true
unread = unread - 1
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 = {
@ -14,9 +14,7 @@ The nice lil shell for {blue}Lua{reset} fanatics!
motd = true,
fuzzy = false,
notifyJobFinish = true,
crimmas = true,
tips = true,
processorSkipList = {}
crimmas = true
}
for optsName, default in pairs(defaultOpts) do

View File

@ -2,7 +2,8 @@ local bait = require 'bait'
local lunacolors = require 'lunacolors'
hilbish.motd = [[
{magenta}Hilbish{reset} blooms in the {blue}midnight.{reset}
Finally at {red}v2.2!{reset} So much {green}documentation improvements{reset}
and 1 single fix for Windows! {blue}.. and a feature they can't use.{reset}
]]
bait.catch('hilbish.init', function()

View File

@ -1,35 +0,0 @@
local bait = require 'bait'
local lunacolors = require 'lunacolors'
local postamble = [[
{yellow}These tips can be disabled with {reset}{invert} hilbish.opts.tips = false {reset}
]]
hilbish.tips = {
'Join the discord and say hi! {blue}https://discord.gg/3PDdcQz{reset}',
'{green}hilbish.alias{reset} interface manages shell aliases. See more detail by running {blue}doc api hilbish.alias.',
'{green}hilbish.appendPath(\'path\'){reset} -> Appends the provided dir to the command path ($PATH)',
'{green}hilbish.completions{reset} -> Used to control suggestions when tab completing.',
'{green}hilbish.message{reset} -> Simple notification system which can be used by other plugins and parts of the shell to notify the user of various actions.',
[[
{green}hilbish.opts{reset} -> Simple toggle or value options a user can set.
You may disable the startup greeting by {invert}hilbish.opts.greeting = false{reset}
]],
[[
{green}hilbish.runner{reset} -> The runner interface contains functions to
manage how Hilbish interprets interactive input. The default runners can run
shell script and Lua code!
]],
[[
Add Lua-written commands with the commander module!
Check the command {blue}doc api commander{reset} or the web docs:
https://rosettea.github.io/Hilbish/docs/api/commander/
]]
}
bait.catch('hilbish.init', function()
if hilbish.interactive and hilbish.opts.tips then
local idx = math.random(1, #hilbish.tips)
print(lunacolors.format('{yellow}🛈 Tip:{reset} ' .. hilbish.tips[idx] .. '\n' .. postamble))
end
end)

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

@ -1,4 +1,4 @@
//go:build pprof
// +build pprof
package main

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)
}

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