2
2
mirror of https://github.com/Hilbis/Hilbish synced 2025-07-12 05:42:02 +00:00

Compare commits

...

42 Commits

Author SHA1 Message Date
eb630b2de0
chore: change back release to moonflower 2025-06-16 17:16:45 -04:00
548de98551
chore: merge from 3.0 branch 2025-06-16 17:16:21 -04:00
49f2bae9e1
feat: add yarn library (#355) 2025-06-15 16:25:07 -04:00
5ca858d112
feat: move readline to golibs (#356) 2025-06-15 15:47:53 -04:00
1bb433dc64
feat: add processors api (#343)
* feat: add processors api

* fix: make processor work properly, implement in exec code

* feat: add names to processors, allow skipping based on name

* feat: add opt to skip processors

* feat: allow processors to set if history should be saved
2025-06-10 18:23:24 -04:00
aec17d1077
chore: merge 2025-05-30 12:28:26 -04:00
049b517b88
feat: emit hilbish.cd on fs.cd (closes #350) 2025-05-30 12:27:57 -04:00
35272ce1ac
feat: implement alias and display for completion groups of type list (#347) 2025-05-04 20:48:56 -04:00
Nadiyar
0036932693
docs: improve existing lunacolors guide (#331)
* docs: extend the lunacolors guide

* docs: add lunacolors change to CHANGELOG

---------

Co-authored-by: sammyette <torchedsammy@gmail.com>
2025-05-04 20:41:19 -04:00
e676c095c2
fix: dont refresh log on regex search error 2025-04-24 12:08:12 -04:00
ef4c925e37
fix: alias resolution for continued input with quotes 2025-04-24 11:27:43 -04:00
80d72a29bb
feat: return values returned by bait hooks (closes #297) 2025-04-23 20:12:05 -04:00
0854b5a769
fix!: don't define hilbish.completion, without the s at the end
the interface is hilbish.completions (s at the end)
2025-04-23 19:36:50 -04:00
2ca99fe831
fix!: get/set env variables via env table (#294)
this removes the old "virtual global table" which allows
getting environment variables via just their names in lua.

this means:
an environment variable (like TERM) would need to be accessed
via the `env` table instead of just using TERM in lua.

`TERM` -> `env.TERM`
they can also be set via `env.VARIABLE = 'value'
2025-04-23 19:33:09 -04:00
8793a733a6
chore: change ver info to 3.0 2025-04-23 19:31:11 -04:00
6827940466
chore: remove print and fix formatting 2025-04-17 22:36:29 -04:00
fde615ff3f
chore: merge 2025-04-17 22:34:23 -04:00
d002c82271
fix: hilbish.run return exitCode instead of runner output table 2025-04-17 22:34:17 -04:00
f64229b52c
fix: set autoflush to true by default for sinks, and flush when reading from sinks (closes #344) 2025-04-17 22:33:50 -04:00
James Dugan
3d5766ac33
fix: hilbish.sink.readAll() function now reads data that doesn't end in a newline (#345) 2025-04-17 22:06:21 -04:00
60edfc00ee
chore: delete some unneeded code and files 2025-04-03 09:08:27 -04:00
6cd294373c
feat: add abbreviations (#340) 2025-04-03 08:45:02 -04:00
02c89b99dd
refactor: decouple sh use in core exec code (#337) 2025-04-03 00:38:35 -04:00
fe4e972fbe
chore: update version info 2025-04-03 00:37:17 -04:00
e4a9e06d2a
chore: merge (again) 2025-04-02 11:12:13 -04:00
487b5fa4f8
ci: checkout with submodules 2025-04-02 11:12:09 -04:00
sammy-ette
946e8e7228 docs: [ci] generate new docs 2025-04-02 15:09:45 +00:00
04206b6a14
docs: upload docs 2025-04-02 11:09:18 -04:00
9ea2a2f332
chore: merge 2025-04-02 11:08:54 -04:00
sammy-ette
364cb1ca2e
fix: add . to dataDir as fallback 2025-04-02 11:08:41 -04:00
sammy-ette
2ba878713c docs: [ci] generate new docs 2025-04-02 14:55:27 +00:00
2678ec723e
fix: push docs 2025-04-02 10:55:11 -04:00
dbf2d80863
fix: remove unused vars for other targets 2025-04-02 10:51:53 -04:00
fab98bc613
feat: search XDG_DATA_DIRS for hilbish files 2025-04-02 10:49:25 -04:00
sammy-ette
7b16cde540 docs: [ci] generate new docs 2025-04-02 13:42:07 +00:00
4150001d8b
fix: make lua implemented hilbish interfaces documented (#335)
* fix: make lua implemented hilbish interfaces documented

* fix: signature link in table of contents

* fix: reduce function list to match in go generated docs

* fix: toc appending

* docs: enable docs for hilbish.messages

* feat: add description gen, and more spacing between param listing

* docs: add more detailed documentation for lua modules

* docs: update hilbish.messages docs

* fix: add description for lua doc'd modules, remove duplicate docs

* docs: add back hilbish.jobs doc

* feat: generate toc for lua modules

* fix: add table heading

* ci: add lua docgen

* docs: put dirs.old doc on 1 line
2025-04-02 09:41:37 -04:00
32ed0d2348
docs: add a bit of extra info in the getting started doc 2025-03-14 18:45:56 -04:00
8731b1a7d1
chore: revert "chore: revert "chore: add 2.4 motd (work in progress)""
revertception
This reverts commit 7fc3f4a569405c86675978341a0c008069b994b9.
2024-12-29 21:44:05 -04:00
4743222044
chore: forward master in sync to v2.3.4 2024-12-28 19:58:00 -04:00
a02cd1d7ef
fix: use global env variables when executing 2024-12-28 19:50:06 -04:00
c969f5ed15
feat: complete hint text on right arrow (#328) 2024-12-22 12:09:57 -04:00
CelestialCrafter
36ce05e85a
fix: handle completion info check error (#330)
* fix: handle completion info check error
fixes Rosettea/Hilbish#329

* make changelog more descriptive
2024-11-22 20:20:43 -04:00
131 changed files with 3500 additions and 4283 deletions

View File

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

View File

@ -1,9 +1,38 @@
# 🎀 Changelog # 🎀 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 ## [2.3.4] - 2024-12-28
### Fixed ### Fixed
- Skip over file and prevent panic if info cannot be retrieved during file completion (due to permission error or anything else) - 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 - 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 ## [2.3.3] - 2024-11-04
### Fixed ### Fixed

View File

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

170
api.go
View File

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

View File

@ -2,14 +2,14 @@ package main
import ( import (
"fmt" "fmt"
"path/filepath"
"go/ast" "go/ast"
"go/doc" "go/doc"
"go/parser" "go/parser"
"go/token" "go/token"
"os"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"os"
"sync" "sync"
md "github.com/atsushinee/go-markdown-generator/doc" md "github.com/atsushinee/go-markdown-generator/doc"
@ -84,6 +84,9 @@ var prefix = map[string]string{
"commander": "c", "commander": "c",
"bait": "b", "bait": "b",
"terminal": "term", "terminal": "term",
"snail": "snail",
"readline": "rl",
"yarn": "yarn",
} }
func getTagsAndDocs(docs string) (map[string][]tag, []string) { func getTagsAndDocs(docs string) (map[string][]tag, []string) {
@ -208,6 +211,10 @@ func setupDocType(mod string, typ *doc.Type) *docPiece {
} }
func setupDoc(mod string, fun *doc.Func) *docPiece { func setupDoc(mod string, fun *doc.Func) *docPiece {
if fun.Doc == "" {
return nil
}
docs := strings.TrimSpace(fun.Doc) docs := strings.TrimSpace(fun.Doc)
tags, parts := getTagsAndDocs(docs) tags, parts := getTagsAndDocs(docs)
@ -216,6 +223,10 @@ func setupDoc(mod string, fun *doc.Func) *docPiece {
goto start 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) { if (!strings.HasPrefix(fun.Name, prefix[mod]) && tags["interface"] == nil) || (strings.ToLower(fun.Name) == "loader" && tags["interface"] == nil) {
return nil return nil
} }
@ -299,10 +310,28 @@ start:
func main() { func main() {
fset := token.NewFileSet() fset := token.NewFileSet()
os.Mkdir("docs", 0777) os.Mkdir("docs", 0777)
os.RemoveAll("docs/api")
os.Mkdir("docs/api", 0777) 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) os.Mkdir("emmyLuaDocs", 0777)
dirs := []string{"./"} dirs := []string{"./", "./util"}
filepath.Walk("golibs/", func(path string, info os.FileInfo, err error) error { filepath.Walk("golibs/", func(path string, info os.FileInfo, err error) error {
if !info.IsDir() { if !info.IsDir() {
return nil return nil
@ -329,7 +358,7 @@ func main() {
pieces := []docPiece{} pieces := []docPiece{}
typePieces := []docPiece{} typePieces := []docPiece{}
mod := l mod := l
if mod == "main" { if mod == "main" || mod == "util" {
mod = "hilbish" mod = "hilbish"
} }
var hasInterfaces bool var hasInterfaces bool
@ -413,6 +442,14 @@ func main() {
interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece) 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{ docs[mod] = module{
Types: filteredTypePieces, Types: filteredTypePieces,
Docs: filteredPieces, Docs: filteredPieces,
@ -423,6 +460,7 @@ func main() {
Fields: docPieceTag("field", tags), Fields: docPieceTag("field", tags),
} }
} }
}
for key, mod := range interfaceModules { for key, mod := range interfaceModules {
docs[key] = *mod docs[key] = *mod
@ -505,7 +543,6 @@ func main() {
mdTable.SetTitle(0, "") mdTable.SetTitle(0, "")
mdTable.SetTitle(1, "") mdTable.SetTitle(1, "")
for i, dps := range modu.Fields { for i, dps := range modu.Fields {
mdTable.SetContent(i, 0, dps.FuncName) mdTable.SetContent(i, 0, dps.FuncName)
mdTable.SetContent(i, 1, strings.Join(dps.Doc, " ")) mdTable.SetContent(i, 1, strings.Join(dps.Doc, " "))
@ -520,7 +557,6 @@ func main() {
mdTable.SetTitle(0, "") mdTable.SetTitle(0, "")
mdTable.SetTitle(1, "") mdTable.SetTitle(1, "")
for i, dps := range modu.Properties { for i, dps := range modu.Properties {
mdTable.SetContent(i, 0, dps.FuncName) mdTable.SetContent(i, 0, dps.FuncName)
mdTable.SetContent(i, 1, strings.Join(dps.Doc, " ")) mdTable.SetContent(i, 1, strings.Join(dps.Doc, " "))

View File

@ -1,7 +1,9 @@
local fs = require 'fs' local fs = require 'fs'
local emmyPattern = '^%-%-%- (.+)' local emmyPattern = '^%-%-%- (.+)'
local modpattern = '^%-+ @module (%w+)' local emmyPattern2 = '^%-%- (.+)'
local modpattern = '^%-+ @module (.+)'
local pieces = {} local pieces = {}
local descriptions = {}
local files = fs.readdir 'nature' local files = fs.readdir 'nature'
for _, fname in ipairs(files) do for _, fname in ipairs(files) do
@ -13,18 +15,25 @@ for _, fname in ipairs(files) do
local mod = header:match(modpattern) local mod = header:match(modpattern)
if not mod then goto continue end if not mod then goto continue end
print(fname, mod)
pieces[mod] = {} pieces[mod] = {}
descriptions[mod] = {}
local docPiece = {} local docPiece = {}
local lines = {} local lines = {}
local lineno = 0 local lineno = 0
local doingDescription = true
for line in f:lines() do for line in f:lines() do
lineno = lineno + 1 lineno = lineno + 1
lines[lineno] = line lines[lineno] = line
if line == header then goto continue2 end if line == header then goto continue2 end
if not line:match(emmyPattern) then 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 line:match '^function' then if line:match '^function' then
local pattern = (string.format('^function %s%%.', mod) .. '(%w+)') local pattern = (string.format('^function %s%%.', mod) .. '(%w+)')
local funcName = line:match(pattern) local funcName = line:match(pattern)
@ -32,10 +41,12 @@ for _, fname in ipairs(files) do
local dps = { local dps = {
description = {}, description = {},
example = {},
params = {} params = {}
} }
local offset = 1 local offset = 1
local doingExample = false
while true do while true do
local prev = lines[lineno - offset] local prev = lines[lineno - offset]
@ -51,19 +62,31 @@ for _, fname in ipairs(files) do
if emmy == 'param' then if emmy == 'param' then
table.insert(dps.params, 1, { table.insert(dps.params, 1, {
name = emmythings[1], name = emmythings[1],
type = emmythings[2] type = emmythings[2],
-- the +1 accounts for space.
description = table.concat(emmythings, ' '):sub(emmythings[1]:len() + 1 + emmythings[2]:len() + 1)
}) })
end 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 else
table.insert(dps.description, 1, docline) table.insert(dps.description, 1, docline)
end end
end
end
offset = offset + 1 offset = offset + 1
else else
break break
end end
end end
pieces[mod][funcName] = dps table.insert(pieces[mod], {funcName, dps})
end end
docPiece = {} docPiece = {}
goto continue2 goto continue2
@ -81,29 +104,82 @@ description: %s
layout: doc layout: doc
menu: menu:
docs: docs:
parent: "Nature" parent: "%s"
--- ---
]] ]]
for iface, dps in pairs(pieces) do for iface, dps in pairs(pieces) do
local mod = iface:match '(%w+)%.' or 'nature' local mod = iface:match '(%w+)%.' or 'nature'
local path = string.format('docs/%s/%s.md', mod, iface) 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
fs.mkdir(fs.dir(path), true) fs.mkdir(fs.dir(path), true)
local f <close> = io.open(path, 'w')
f:write(string.format(header, 'Module', iface, 'No description.'))
print(f)
print(mod, path) local exists = pcall(fs.stat, path)
local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish'
for func, docs in pairs(dps) do local f <close> = io.open(path, newOrNotNature and 'r+' or 'w+')
f:write(string.format('<hr>\n<div id=\'%s\'>', func)) 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]
local sig = string.format('%s.%s(', iface, func) local sig = string.format('%s.%s(', iface, func)
local params = ''
for idx, param in ipairs(docs.params) do for idx, param in ipairs(docs.params) do
sig = sig .. ((param.name:gsub('%?$', ''))) sig = sig .. param.name:gsub('%?$', '')
if idx ~= #docs.params then sig = sig .. ', ' end params = params .. param.name:gsub('%?$', '')
if idx ~= #docs.params then
sig = sig .. ', '
params = params .. ', '
end
end end
sig = sig .. ')' 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'> <h4 class='heading'>
%s %s
@ -121,6 +197,11 @@ for iface, dps in pairs(pieces) do
end end
for _, param in ipairs(docs.params) do for _, param in ipairs(docs.params) do
f:write(string.format('`%s` **`%s`** \n', param.name:gsub('%?$', ''), param.type)) 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')))
end end
--[[ --[[
local params = table.filter(docs, function(t) 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 { if len(fileCompletions) != 0 {
for _, f := range fileCompletions { for _, f := range fileCompletions {
fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
continue continue
} }
completions = append(completions, f) completions = append(completions, f)
@ -115,7 +115,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) {
// get basename from matches // get basename from matches
for _, match := range matches { for _, match := range matches {
// check if we have execute permissions for our match // check if we have execute permissions for our match
err := findExecutable(match, true, false) err := util.FindExecutable(match, true, false)
if err != nil { if err != nil {
continue continue
} }

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

@ -0,0 +1,67 @@
---
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

@ -1,103 +0,0 @@
---
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

@ -0,0 +1,135 @@
---
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

@ -0,0 +1,39 @@
---
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

@ -54,29 +54,16 @@ end)
## Functions ## 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="#runner.lua">lua(cmd)</a>|Evaluates `cmd` as Lua input. This is the same as using `dofile`|
|<a href="#runner.sh">sh(cmd)</a>|Runs a command in Hilbish's shell script interpreter.| |<a href="#sh">sh()</a>|nil|
|<a href="#setMode">setMode(mode)</a>|**NOTE: This function is deprecated and will be removed in 3.0**|
<hr> |<a href="#setCurrent">setCurrent(name)</a>|Sets Hilbish's runner mode by name.|
<div id='runner.setMode'> |<a href="#set">set(name, runner)</a>|*Sets* a runner by name. The difference between this function and|
<h4 class='heading'> |<a href="#run">run(input, priv)</a>|Runs `input` with the currently set Hilbish runner.|
hilbish.runner.setMode(cb) |<a href="#getCurrent">getCurrent()</a>|Returns the current runner by name.|
<a href="#runner.setMode" class='heading-link'> |<a href="#get">get(name)</a>|Get a runner by name.|
<i class="fas fa-paperclip"></i> |<a href="#exec">exec(cmd, runnerName)</a>|Executes `cmd` with a runner.|
</a> |<a href="#add">add(name, runner)</a>|Adds a runner to the table of available runners.|
</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> <hr>
<div id='runner.lua'> <div id='runner.lua'>
@ -97,20 +84,164 @@ or `load`, but is appropriated for the runner interface.
</div> </div>
<hr> <hr>
<div id='runner.sh'> <div id='add'>
<h4 class='heading'> <h4 class='heading'>
hilbish.runner.sh(cmd) hilbish.runner.add(name, runner)
<a href="#runner.sh" class='heading-link'> <a href="#add" class='heading-link'>
<i class="fas fa-paperclip"></i> <i class="fas fa-paperclip"></i>
</a> </a>
</h4> </h4>
Runs a command in Hilbish's shell script interpreter. Adds a runner to the table of available runners.
This is the equivalent of using `source`. If runner is a table, it must have the run function in it.
#### Parameters #### Parameters
`string` **`cmd`** `name` **`string`**
Name of the runner
`runner` **`function|table`**
</div> </div>
<hr>
<div id='exec'>
<h4 class='heading'>
hilbish.runner.exec(cmd, runnerName)
<a href="#exec" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Executes `cmd` with a runner.
If `runnerName` is not specified, it uses the default Hilbish runner.
#### Parameters
`cmd` **`string`**
`runnerName` **`string?`**
</div>
<hr>
<div id='get'>
<h4 class='heading'>
hilbish.runner.get(name)
<a href="#get" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Get a runner by name.
#### Parameters
`name` **`string`**
Name of the runner to retrieve.
</div>
<hr>
<div id='getCurrent'>
<h4 class='heading'>
hilbish.runner.getCurrent()
<a href="#getCurrent" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Returns the current runner by name.
#### Parameters
This function has no parameters.
</div>
<hr>
<div id='run'>
<h4 class='heading'>
hilbish.runner.run(input, priv)
<a href="#run" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Runs `input` with the currently set Hilbish runner.
This method is how Hilbish executes commands.
`priv` is an optional boolean used to state if the input should be saved to history.
#### Parameters
`input` **`string`**
`priv` **`bool`**
</div>
<hr>
<div id='set'>
<h4 class='heading'>
hilbish.runner.set(name, runner)
<a href="#set" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
*Sets* a runner by name. The difference between this function and
add, is set will *not* check if the named runner exists.
The runner table must have the run function in it.
#### Parameters
`name` **`string`**
`runner` **`table`**
</div>
<hr>
<div id='setCurrent'>
<h4 class='heading'>
hilbish.runner.setCurrent(name)
<a href="#setCurrent" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
Sets Hilbish's runner mode by name.
#### Parameters
`name` **`string`**
</div>
<hr>
<div id='setMode'>
<h4 class='heading'>
hilbish.runner.setMode(mode)
<a href="#setMode" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
**NOTE: This function is deprecated and will be removed in 3.0**
Use `hilbish.runner.setCurrent` instead.
This is the same as the `hilbish.runnerMode` function.
It takes a callback, which will be used to execute all interactive input.
Or a string which names the runner mode to use.
#### Parameters
`mode` **`string|function`**
</div>
<hr>
<div id='sh'>
<h4 class='heading'>
hilbish.runner.sh()
<a href="#sh" class='heading-link'>
<i class="fas fa-paperclip"></i>
</a>
</h4>
#### Parameters
This function has no parameters.
</div>

66
docs/api/readline.md Normal file
View File

@ -0,0 +1,66 @@
---
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.

50
docs/api/snail.md Normal file
View File

@ -0,0 +1,50 @@
---
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.

51
docs/api/yarn.md Normal file
View File

@ -0,0 +1,51 @@
---
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,6 +56,50 @@ return {cg, cg2}, prefix
Which looks like this: Which looks like this:
{{< video src="https://safe.saya.moe/t4CiLK6dgPbD.mp4" >}} {{< 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 # Completion Handler
Like most parts of Hilbish, it's made to be extensible and Like most parts of Hilbish, it's made to be extensible and
customizable. The default handler for completions in general can customizable. The default handler for completions in general can

View File

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

View File

@ -53,8 +53,39 @@ 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: 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` `cp /usr/share/hilbish/.hilbishrc.lua ~/.config/hilbish/init.lua`
Now you can get to editing it. Since it's just a Lua file, having basic Now we can get to customization!
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 If we closely examine a small snippet of the default config:
available. To see them, you can run the `doc` command. This also works as ```lua
general documentation for other things. -- 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.

View File

@ -43,5 +43,29 @@ The notification. The properties are defined in the link above.
<hr> <hr>
+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something ## hilbish.cd
like yanking or pasting text. See `doc vim-mode actions` for more info. 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>

View File

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

View File

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

76
docs/nature/doc.md Normal file
View File

@ -0,0 +1,76 @@
---
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
View File

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

View File

@ -7,27 +7,6 @@ local hilbish = {}
--- @param cmd string --- @param cmd string
function hilbish.aliases.add(alias, cmd) end 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. --- Return binaries/executables based on the provided parameters.
--- This function is meant to be used as a helper in a command completion handler. --- This function is meant to be used as a helper in a command completion handler.
--- ---
@ -131,24 +110,6 @@ function hilbish.prompt(str, typ) end
--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. --- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs.
function hilbish.read(prompt) end function hilbish.read(prompt) end
--- Runs `cmd` in Hilbish's shell script interpreter.
--- The `streams` parameter specifies the output and input streams the command should use.
--- For example, to write command output to a sink.
--- As a table, the caller can directly specify the standard output, error, and input
--- streams of the command with the table keys `out`, `err`, and `input` respectively.
--- As a boolean, it specifies whether the command should use standard output or return its output streams.
---
function hilbish.run(cmd, streams) end
--- Sets the execution/runner mode for interactive Hilbish.
--- This determines whether Hilbish wll try to run input as Lua
--- and/or sh or only do one of either.
--- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua),
--- sh, and lua. It also accepts a function, to which if it is passed one
--- will call it to execute user input instead.
--- Read [about runner mode](../features/runner-mode) for more information.
function hilbish.runnerMode(mode) end
--- Executed the `cb` function after a period of `time`. --- Executed the `cb` function after a period of `time`.
--- This creates a Timer that starts ticking immediately. --- This creates a Timer that starts ticking immediately.
function hilbish.timeout(cb, time) end function hilbish.timeout(cb, time) end
@ -168,28 +129,6 @@ function hilbish.jobs:foreground() end
--- or `load`, but is appropriated for the runner interface. --- or `load`, but is appropriated for the runner interface.
function hilbish.runner.lua(cmd) end 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. --- Starts running the job.
function hilbish.jobs:start() end function hilbish.jobs:start() end
@ -200,10 +139,6 @@ function hilbish.jobs:stop() end
--- It will throw if any error occurs. --- It will throw if any error occurs.
function hilbish.module.load(path) end 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. --- Starts a timer.
function hilbish.timers:start() end function hilbish.timers:start() end
@ -262,4 +197,26 @@ function hilbish.timers.create(type, time, callback) end
--- Retrieves a timer via its ID. --- Retrieves a timer via its ID.
function hilbish.timers.get(id) end 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 return hilbish

32
emmyLuaDocs/readline.lua Normal file
View File

@ -0,0 +1,32 @@
--- @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

16
emmyLuaDocs/snail.lua Normal file
View File

@ -0,0 +1,16 @@
--- @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

83
emmyLuaDocs/util.lua Normal file
View File

@ -0,0 +1,83 @@
--- @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

9
emmyLuaDocs/yarn.lua Normal file
View File

@ -0,0 +1,9 @@
--- @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

532
exec.go
View File

@ -1,215 +1,26 @@
package main package main
import ( import (
"bytes"
"context"
"errors" "errors"
"os/exec"
"fmt" "fmt"
"io"
"os" "os"
"os/signal"
"path/filepath"
"runtime"
"strings" "strings"
"syscall"
"time"
"hilbish/util"
rt "github.com/arnodel/golua/runtime" rt "github.com/arnodel/golua/runtime"
"mvdan.cc/sh/v3/shell"
//"github.com/yuin/gopher-lua/parse" //"github.com/yuin/gopher-lua/parse"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/expand"
) )
var errNotExec = errors.New("not executable") var errNotExec = errors.New("not executable")
var errNotFound = errors.New("not found") var errNotFound = errors.New("not found")
var runnerMode rt.Value = rt.StringValue("hybrid") var runnerMode rt.Value = rt.NilValue
type streams struct {
stdout io.Writer
stderr io.Writer
stdin io.Reader
}
type execError struct{
typ string
cmd string
code int
colon bool
err error
}
func (e execError) Error() string {
return fmt.Sprintf("%s: %s", e.cmd, e.typ)
}
func (e execError) sprint() error {
sep := " "
if e.colon {
sep = ": "
}
return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error())
}
func isExecError(err error) (execError, bool) {
if exErr, ok := err.(execError); ok {
return exErr, true
}
fields := strings.Split(err.Error(), ": ")
knownTypes := []string{
"not-found",
"not-executable",
}
if len(fields) > 1 && contains(knownTypes, fields[1]) {
var colon bool
var e error
switch fields[1] {
case "not-found":
e = errNotFound
case "not-executable":
colon = true
e = errNotExec
}
return execError{
cmd: fields[0],
typ: fields[1],
colon: colon,
err: e,
}, true
}
return execError{}, false
}
func runInput(input string, priv bool) { func runInput(input string, priv bool) {
running = true running = true
cmdString := aliases.Resolve(input) runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run"))
hooks.Emit("command.preexec", input, cmdString) _, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv))
rerun:
var exitCode uint8
var err error
var cont bool
var newline 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, newline, 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, newline, err = handleSh(input)
}
} else {
// can only be a string or function so
var runnerErr error
input, exitCode, cont, newline, runnerErr, err = runLuaRunner(currentRunner, input)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) 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 = continuePrompt(input, newline)
if err == nil {
goto rerun
} else if err == io.EOF {
lr.SetPrompt(fmtPrompt(prompt))
}
}
if err != nil && err != io.EOF {
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, newline bool) (string, error) {
for {
/*
if strings.HasSuffix(input, "\\") {
input = strings.TrimSuffix(input, "\\") + "\n"
}
*/
in, err := continuePrompt(input, newline)
if err != nil {
lr.SetPrompt(fmtPrompt(prompt))
return input, err
}
return in, nil
}
}
func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, newline 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, 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
}
if nl, ok := runner.Get(rt.StringValue("newline")).TryBool(); ok {
newline = nl
}
return
} }
func handleLua(input string) (string, uint8, error) { func handleLua(input string) (string, uint8, error) {
@ -239,326 +50,13 @@ func handleLua(input string) (string, uint8, error) {
return cmdString, 125, err return cmdString, 125, err
} }
func handleSh(cmdString string) (input string, exitCode uint8, cont bool, newline bool, runErr error) {
shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh"))
var err error
input, exitCode, cont, newline, runErr, err = runLuaRunner(shRunner, cmdString)
if err != nil {
runErr = err
}
return
}
func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline bool, e error) {
_, _, err := execCommand(cmdString, nil)
if err != nil {
// If input is incomplete, start multiline prompting
if syntax.IsIncomplete(err) {
if !interactive {
return cmdString, 126, false, false, err
}
newline := false
if strings.Contains(err.Error(), "unclosed here-document") {
newline = true
}
return cmdString, 126, true, newline, err
} else {
if code, ok := interp.IsExitStatus(err); ok {
return cmdString, code, false, false, nil
} else {
return cmdString, 126, false, false, err
}
}
}
return cmdString, 0, false, false, nil
}
// Run command in sh interpreter
func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return nil, nil, err
}
if strms == nil {
strms = &streams{}
}
if strms.stdout == nil {
strms.stdout = os.Stdout
}
if strms.stderr == nil {
strms.stderr = os.Stderr
}
if strms.stdin == nil {
strms.stdin = os.Stdin
}
interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner)
interp.Env(nil)(runner)
buf := new(bytes.Buffer)
printer := syntax.NewPrinter()
var bg bool
for _, stmt := range file.Stmts {
bg = false
if stmt.Background {
bg = true
printer.Print(buf, stmt.Cmd)
stmtStr := buf.String()
buf.Reset()
jobs.add(stmtStr, []string{}, "")
}
interp.ExecHandler(execHandle(bg))(runner)
err = runner.Run(context.TODO(), stmt)
if err != nil {
return strms.stdout, strms.stderr, err
}
}
return strms.stdout, strms.stderr, nil
}
func execHandle(bg bool) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
_, argstring := splitInput(strings.Join(args, " "))
// i dont really like this but it works
if aliases.All()[args[0]] != "" {
for i, arg := range args {
if strings.Contains(arg, " ") {
args[i] = fmt.Sprintf("\"%s\"", arg)
}
}
_, argstring = splitInput(strings.Join(args, " "))
// If alias was found, use command alias
argstring = aliases.Resolve(argstring)
var err error
args, err = shell.Fields(argstring, nil)
if err != nil {
return err
}
}
// If command is defined in Lua then run it
luacmdArgs := rt.NewTable()
for i, str := range args[1:] {
luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str))
}
hc := interp.HandlerCtx(ctx)
if cmd := cmds.Commands[args[0]]; cmd != nil {
stdin := newSinkInput(hc.Stdin)
stdout := newSinkOutput(hc.Stdout)
stderr := newSinkOutput(hc.Stderr)
sinks := rt.NewTable()
sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud))
sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud))
sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud))
sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud))
t := rt.NewThread(l)
sig := make(chan os.Signal)
exit := make(chan bool)
luaexitcode := rt.IntValue(63)
var err error
go func() {
defer func() {
if r := recover(); r != nil {
exit <- true
}
}()
signal.Notify(sig, os.Interrupt)
select {
case <-sig:
t.KillContext()
return
}
}()
go func() {
luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks))
exit <- true
}()
<-exit
if err != nil {
fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error())
return interp.NewExitStatus(1)
}
var exitcode uint8
if code, ok := luaexitcode.TryInt(); ok {
exitcode = uint8(code)
} else if luaexitcode != rt.NilValue {
// deregister commander
delete(cmds.Commands, args[0])
fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0])
}
return interp.NewExitStatus(exitcode)
}
path, 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
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 := 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) (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 file, 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 path, nil
}
}
return "", os.ErrNotExist
}
func splitInput(input string) ([]string, string) { func splitInput(input string) ([]string, string) {
// end my suffering // end my suffering
// TODO: refactor this garbage // TODO: refactor this garbage
quoted := false quoted := false
startlastcmd := false
lastcmddone := false
cmdArgs := []string{} cmdArgs := []string{}
sb := &strings.Builder{} sb := &strings.Builder{}
cmdstr := &strings.Builder{} cmdstr := &strings.Builder{}
lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1)
for _, r := range input { for _, r := range input {
if r == '"' { if r == '"' {
@ -574,22 +72,6 @@ func splitInput(input string) ([]string, string) {
// if not quoted and there's a space then add to cmdargs // if not quoted and there's a space then add to cmdargs
cmdArgs = append(cmdArgs, sb.String()) cmdArgs = append(cmdArgs, sb.String())
sb.Reset() 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 { } else {
sb.WriteRune(r) sb.WriteRune(r)
} }
@ -601,11 +83,3 @@ func splitInput(input string) ([]string, string) {
return cmdArgs, cmdstr.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)
}

2
go.mod
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import (
// initList - List display details. Because of the way alternative completions // initList - List display details. Because of the way alternative completions
// are handled, MaxLength cannot be set when there are alternative completions. // are handled, MaxLength cannot be set when there are alternative completions.
func (g *CompletionGroup) initList(rl *Instance) { func (g *CompletionGroup) initList(rl *Readline) {
// We may only ever have two different // We may only ever have two different
// columns: (suggestions, and alternatives) // columns: (suggestions, and alternatives)
@ -53,7 +53,7 @@ func (g *CompletionGroup) initList(rl *Instance) {
// moveTabListHighlight - Moves the highlighting for currently selected completion item (list display) // 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) // We don't care about the x, because only can have 2 columns of selectable choices (--long and -s)
func (g *CompletionGroup) moveTabListHighlight(rl *Instance, x, y int) (done bool, next bool) { func (g *CompletionGroup) moveTabListHighlight(rl *Readline, x, y int) (done bool, next bool) {
// We dont' pass to x, because not managed by callers // We dont' pass to x, because not managed by callers
g.tcPosY += x g.tcPosY += x
@ -153,7 +153,7 @@ func (g *CompletionGroup) moveTabListHighlight(rl *Instance, x, y int) (done boo
} }
// writeList - A list completion string // writeList - A list completion string
func (g *CompletionGroup) writeList(rl *Instance) (comp string) { func (g *CompletionGroup) writeList(rl *Readline) (comp string) {
// Print group title and adjust offset if there is one. // Print group title and adjust offset if there is one.
if g.Name != "" { if g.Name != "" {
@ -217,6 +217,11 @@ func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
alt = strings.Repeat(" ", maxLengthAlt+1) // + 2 to keep account of spaces 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
description := g.Descriptions[g.Suggestions[i]] description := g.Descriptions[g.Suggestions[i]]
if len(description) > maxDescWidth { if len(description) > maxDescWidth {
@ -244,7 +249,7 @@ func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
return return
} }
func (rl *Instance) getListPad() (pad int) { func (rl *Readline) getListPad() (pad int) {
for _, group := range rl.tcGroups { for _, group := range rl.tcGroups {
if group.DisplayType == TabDisplayList { if group.DisplayType == TabDisplayList {
for i := range group.Suggestions { 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 // 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. // a working completion group either immediately, or later on. Generally defered.
func (g *CompletionGroup) initMap(rl *Instance) { func (g *CompletionGroup) initMap(rl *Readline) {
// We make the map anyway, especially if we need to use it later // We make the map anyway, especially if we need to use it later
if g.Descriptions == nil { if g.Descriptions == nil {
@ -35,7 +35,7 @@ func (g *CompletionGroup) initMap(rl *Instance) {
} }
// moveTabMapHighlight - Moves the highlighting for currently selected completion item (map display) // moveTabMapHighlight - Moves the highlighting for currently selected completion item (map display)
func (g *CompletionGroup) moveTabMapHighlight(rl *Instance, x, y int) (done bool, next bool) { func (g *CompletionGroup) moveTabMapHighlight(rl *Readline, x, y int) (done bool, next bool) {
g.tcPosY += x g.tcPosY += x
g.tcPosY += y g.tcPosY += y
@ -72,7 +72,7 @@ func (g *CompletionGroup) moveTabMapHighlight(rl *Instance, x, y int) (done bool
} }
// writeMap - A map or list completion string // writeMap - A map or list completion string
func (g *CompletionGroup) writeMap(rl *Instance) (comp string) { func (g *CompletionGroup) writeMap(rl *Readline) (comp string) {
if g.Name != "" { if g.Name != "" {
// Print group title (changes with line returns depending on type) // Print group title (changes with line returns depending on type)

View File

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

View File

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

View File

@ -1,9 +1,10 @@
//go:build plan9
// +build plan9 // +build plan9
package readline package readline
import "errors" import "errors"
func (rl *Instance) launchEditor(multiline []rune) ([]rune, error) { func (rl *Readline) launchEditor(multiline []rune) ([]rune, error) {
return rl.line, errors.New("Not currently supported on Plan 9") 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) // 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, // 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. // which might be useful if the editor has builtin filetype plugin functionality.
func (rl *Instance) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) { func (rl *Readline) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
name, err := rl.writeTempFile([]byte(string(multiline)), filename) name, err := rl.writeTempFile([]byte(string(multiline)), filename)
if err != nil { if err != nil {
return multiline, err return multiline, err

View File

@ -5,6 +5,6 @@ package readline
import "errors" import "errors"
// StartEditorWithBuffer - Not implemented on Windows platforms. // StartEditorWithBuffer - Not implemented on Windows platforms.
func (rl *Instance) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) { func (rl *Readline) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
return rl.line, errors.New("Not currently supported on Windows") 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 // AddEvent registers a new keypress handler
func (rl *Instance) AddEvent(keyPress string, callback func(string, []rune, int) *EventReturn) { func (rl *Readline) AddEvent(keyPress string, callback func(string, []rune, int) *EventReturn) {
rl.evtKeyPress[keyPress] = callback rl.evtKeyPress[keyPress] = callback
} }
// DelEvent deregisters an existing keypress handler // DelEvent deregisters an existing keypress handler
func (rl *Instance) DelEvent(keyPress string) { func (rl *Readline) DelEvent(keyPress string) {
delete(rl.evtKeyPress, keyPress) delete(rl.evtKeyPress, keyPress)
} }

View File

@ -5,13 +5,13 @@ import "regexp"
// SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders // SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders
// them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed. // them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed.
/* /*
func (rl *Instance) SetHintText(s string) { func (rl *Readline) SetHintText(s string) {
rl.hintText = []rune(s) rl.hintText = []rune(s)
rl.renderHelpers() rl.renderHelpers()
} }
*/ */
func (rl *Instance) getHintText() { func (rl *Readline) getHintText() {
if !rl.modeAutoFind && !rl.modeTabFind { if !rl.modeAutoFind && !rl.modeTabFind {
// Return if no hints provided by the user/engine // Return if no hints provided by the user/engine
@ -27,7 +27,7 @@ func (rl *Instance) getHintText() {
} }
// writeHintText - only writes the hint text and computes its offsets. // writeHintText - only writes the hint text and computes its offsets.
func (rl *Instance) writeHintText() { func (rl *Readline) writeHintText() {
if len(rl.hintText) == 0 { if len(rl.hintText) == 0 {
//rl.hintY = 0 //rl.hintY = 0
return return
@ -52,7 +52,14 @@ func (rl *Instance) writeHintText() {
} }
} }
func (rl *Instance) resetHintText() { func (rl *Readline) resetHintText() {
//rl.hintY = 0 //rl.hintY = 0
rl.hintText = []rune{} rl.hintText = []rune{}
} }
func (rl *Readline) insertHintText() {
if len(rl.hintText) != 0 {
// fill in hint text
rl.insert(rl.hintText)
}
}

View File

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

View File

@ -4,12 +4,12 @@ import "regexp"
// SetInfoText - a nasty function to force writing a new info text. It does not update helpers, it just renders // SetInfoText - a nasty function to force writing a new info text. It does not update helpers, it just renders
// them, so the info will survive until the helpers (thus including the info) will be updated/recomputed. // them, so the info will survive until the helpers (thus including the info) will be updated/recomputed.
func (rl *Instance) SetInfoText(s string) { func (rl *Readline) SetInfoText(s string) {
rl.infoText = []rune(s) rl.infoText = []rune(s)
rl.renderHelpers() rl.renderHelpers()
} }
func (rl *Instance) getInfoText() { func (rl *Readline) getInfoText() {
if !rl.modeAutoFind && !rl.modeTabFind { if !rl.modeAutoFind && !rl.modeTabFind {
// Return if no infos provided by the user/engine // Return if no infos provided by the user/engine
@ -25,7 +25,7 @@ func (rl *Instance) getInfoText() {
} }
// writeInfoText - only writes the info text and computes its offsets. // writeInfoText - only writes the info text and computes its offsets.
func (rl *Instance) writeInfoText() { func (rl *Readline) writeInfoText() {
if len(rl.infoText) == 0 { if len(rl.infoText) == 0 {
rl.infoY = 0 rl.infoY = 0
return return
@ -50,7 +50,7 @@ func (rl *Instance) writeInfoText() {
} }
} }
func (rl *Instance) resetInfoText() { func (rl *Readline) resetInfoText() {
rl.infoY = 0 rl.infoY = 0
rl.infoText = []rune{} rl.infoText = []rune{}
} }

View File

@ -5,12 +5,16 @@ import (
"os" "os"
"regexp" "regexp"
"sync" "sync"
"github.com/arnodel/golua/lib/packagelib"
) )
// Instance is used to encapsulate the parameter group and run time of any given // Instance is used to encapsulate the parameter group and run time of any given
// readline instance so that you can reuse the readline API for multiple entry // readline instance so that you can reuse the readline API for multiple entry
// captures without having to repeatedly unload configuration. // captures without having to repeatedly unload configuration.
type Instance struct {
// #type
type Readline struct {
// //
// Input Modes ------------------------------------------------------------------------------- // Input Modes -------------------------------------------------------------------------------
@ -206,11 +210,13 @@ type Instance struct {
RawInputCallback func([]rune) // called on all input RawInputCallback func([]rune) // called on all input
bufferedOut *bufio.Writer bufferedOut *bufio.Writer
Loader packagelib.Loader
} }
// NewInstance is used to create a readline instance and initialise it with sane defaults. // NewInstance is used to create a readline instance and initialise it with sane defaults.
func NewInstance() *Instance { func NewInstance() *Readline {
rl := new(Instance) rl := new(Readline)
// Prompt // Prompt
rl.Multiline = false rl.Multiline = false
@ -240,12 +246,14 @@ func NewInstance() *Instance {
var err error var err error
rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine)) rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine))
if err != nil { if err != nil {
rl.RefreshPromptLog(err.Error()) //rl.RefreshPromptLog(err.Error())
rl.infoText = []rune(Red("Failed to match search regexp")) rl.infoText = []rune(Red("Failed to match search regexp"))
} }
for _, hay := range haystack { for _, hay := range haystack {
if rl.regexSearch == nil { continue } if rl.regexSearch == nil {
continue
}
if rl.regexSearch.MatchString(hay) { if rl.regexSearch.MatchString(hay) {
suggs = append(suggs, hay) suggs = append(suggs, hay)
} }
@ -256,6 +264,11 @@ func NewInstance() *Instance {
rl.bufferedOut = bufio.NewWriter(os.Stdout) rl.bufferedOut = bufio.NewWriter(os.Stdout)
rl.Loader = packagelib.Loader{
Name: "readline",
Load: rl.luaLoader,
}
// Registers // Registers
rl.initRegisters() rl.initRegisters()

View File

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

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

@ -0,0 +1,276 @@
// 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

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

View File

@ -2,16 +2,6 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// Package terminal provides support functions for dealing with terminals, as
// commonly found on UNIX systems.
//
// Putting a terminal into raw mode is the most common requirement:
//
// oldState, err := terminal.MakeRaw(0)
// if err != nil {
// panic(err)
// }
// defer terminal.Restore(0, oldState)
package readline package readline
import ( import (

View File

@ -2,18 +2,9 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build windows
// +build windows // +build windows
// Package terminal provides support functions for dealing with terminals, as
// commonly found on UNIX systems.
//
// Putting a terminal into raw mode is the most common requirement:
//
// oldState, err := terminal.MakeRaw(0)
// if err != nil {
// panic(err)
// }
// defer terminal.Restore(0, oldState)
package readline package readline
import ( import (
@ -70,4 +61,3 @@ func GetSize(fd int) (width, height int, err error) {
} }
return int(info.Size.X), int(info.Size.Y), nil return int(info.Size.X), int(info.Size.Y), nil
} }

View File

@ -13,7 +13,7 @@ var rxMultiline = regexp.MustCompile(`[\r\n]+`)
// Readline displays the readline prompt. // Readline displays the readline prompt.
// It will return a string (user entered data) or an error. // It will return a string (user entered data) or an error.
func (rl *Instance) Readline() (string, error) { func (rl *Readline) Readline() (string, error) {
fd := int(os.Stdin.Fd()) fd := int(os.Stdin.Fd())
state, err := MakeRaw(fd) state, err := MakeRaw(fd)
if err != nil { if err != nil {
@ -183,7 +183,7 @@ func (rl *Instance) Readline() (string, error) {
if rl.modeTabFind { if rl.modeTabFind {
rl.backspaceTabFind() rl.backspaceTabFind()
} else { } else {
if (rl.pos < len(rl.line)) { if rl.pos < len(rl.line) {
rl.deleteBackspace(true) rl.deleteBackspace(true)
} }
} }
@ -545,7 +545,7 @@ func (rl *Instance) Readline() (string, error) {
// editorInput is an unexported function used to determine what mode of text // editorInput is an unexported function used to determine what mode of text
// entry readline is currently configured for and then update the line entries // entry readline is currently configured for and then update the line entries
// accordingly. // accordingly.
func (rl *Instance) editorInput(r []rune) { func (rl *Readline) editorInput(r []rune) {
if len(r) == 0 { if len(r) == 0 {
return return
} }
@ -595,7 +595,7 @@ func (rl *Instance) editorInput(r []rune) {
// viEscape - In case th user is using Vim input, and the escape sequence has not // viEscape - In case th user is using Vim input, and the escape sequence has not
// been handled by other cases, we dispatch it to Vim and handle a few cases here. // been handled by other cases, we dispatch it to Vim and handle a few cases here.
func (rl *Instance) viEscape(r []rune) { func (rl *Readline) viEscape(r []rune) {
// Sometimes the escape sequence is interleaved with another one, // Sometimes the escape sequence is interleaved with another one,
// but key strokes might be in the wrong order, so we double check // but key strokes might be in the wrong order, so we double check
@ -611,7 +611,7 @@ func (rl *Instance) viEscape(r []rune) {
} }
} }
func (rl *Instance) escapeSeq(r []rune) { func (rl *Readline) escapeSeq(r []rune) {
switch string(r) { switch string(r) {
// Vim escape sequences & dispatching -------------------------------------------------------- // Vim escape sequences & dispatching --------------------------------------------------------
case string(charEscape): case string(charEscape):
@ -707,6 +707,9 @@ func (rl *Instance) escapeSeq(r []rune) {
rl.renderHelpers() rl.renderHelpers()
return return
} }
rl.insertHintText()
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) || if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) { (rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
rl.moveCursorByAdjust(1) rl.moveCursorByAdjust(1)
@ -756,7 +759,7 @@ func (rl *Instance) escapeSeq(r []rune) {
if rl.modeTabFind { if rl.modeTabFind {
rl.backspaceTabFind() rl.backspaceTabFind()
} else { } else {
if (rl.pos < len(rl.line)) { if rl.pos < len(rl.line) {
rl.deleteBackspace(true) rl.deleteBackspace(true)
} }
} }
@ -887,7 +890,7 @@ func (rl *Instance) escapeSeq(r []rune) {
} }
} }
func (rl *Instance) carridgeReturn() { func (rl *Readline) carridgeReturn() {
rl.moveCursorByAdjust(len(rl.line)) rl.moveCursorByAdjust(len(rl.line))
rl.updateHelpers() rl.updateHelpers()
rl.clearHelpers() rl.clearHelpers()
@ -921,7 +924,7 @@ func isMultiline(r []rune) bool {
return false return false
} }
func (rl *Instance) allowMultiline(data []byte) bool { func (rl *Readline) allowMultiline(data []byte) bool {
rl.clearHelpers() rl.clearHelpers()
printf("\r\nWARNING: %d bytes of multiline data was dumped into the shell!", len(data)) printf("\r\nWARNING: %d bytes of multiline data was dumped into the shell!", len(data))
for { for {

View File

@ -23,7 +23,7 @@ type registers struct {
mutex *sync.Mutex mutex *sync.Mutex
} }
func (rl *Instance) initRegisters() { func (rl *Readline) initRegisters() {
rl.registers = &registers{ rl.registers = &registers{
num: make(map[int][]rune, 10), num: make(map[int][]rune, 10),
alpha: make(map[string][]rune, 52), alpha: make(map[string][]rune, 52),
@ -36,7 +36,7 @@ func (rl *Instance) initRegisters() {
// the number of Vim iterations and we save the resulting string to the appropriate buffer. // the number of Vim iterations and we save the resulting string to the appropriate buffer.
// It's the same as saveToRegisterTokenize, but without the need to generate tokenized & // It's the same as saveToRegisterTokenize, but without the need to generate tokenized &
// cursor-pos-actualized versions of the input line. // cursor-pos-actualized versions of the input line.
func (rl *Instance) saveToRegister(adjust int) { func (rl *Readline) saveToRegister(adjust int) {
// Get the current cursor position and go the length specified. // Get the current cursor position and go the length specified.
var begin = rl.pos var begin = rl.pos
@ -66,7 +66,7 @@ func (rl *Instance) saveToRegister(adjust int) {
// saveToRegisterTokenize - Passing a function that will move around the line in the desired way, we get // saveToRegisterTokenize - Passing a function that will move around the line in the desired way, we get
// the number of Vim iterations and we save the resulting string to the appropriate buffer. Because we // the number of Vim iterations and we save the resulting string to the appropriate buffer. Because we
// need the cursor position to be really moved around between calls to the jumper, we also need the tokeniser. // need the cursor position to be really moved around between calls to the jumper, we also need the tokeniser.
func (rl *Instance) saveToRegisterTokenize(tokeniser tokeniser, jumper func(tokeniser) int, vii int) { func (rl *Readline) saveToRegisterTokenize(tokeniser tokeniser, jumper func(tokeniser) int, vii int) {
// The register is going to have to heavily manipulate the cursor position. // The register is going to have to heavily manipulate the cursor position.
// Remember the original one first, for the end. // Remember the original one first, for the end.
@ -104,11 +104,11 @@ func (rl *Instance) saveToRegisterTokenize(tokeniser tokeniser, jumper func(toke
// saveBufToRegister - Instead of computing the buffer ourselves based on an adjust, // saveBufToRegister - Instead of computing the buffer ourselves based on an adjust,
// let the caller pass directly this buffer, yet relying on the register system to // let the caller pass directly this buffer, yet relying on the register system to
// determine which register will store the buffer. // determine which register will store the buffer.
func (rl *Instance) saveBufToRegister(buffer []rune) { func (rl *Readline) saveBufToRegister(buffer []rune) {
rl.SetRegisterBuf(string(rl.registers.currentRegister), buffer) rl.SetRegisterBuf(string(rl.registers.currentRegister), buffer)
} }
func (rl *Instance) SetRegisterBuf(reg string, buffer []rune) { func (rl *Readline) SetRegisterBuf(reg string, buffer []rune) {
// We must make an immutable version of the buffer first. // We must make an immutable version of the buffer first.
buf := string(buffer) buf := string(buffer)
@ -141,7 +141,7 @@ func (rl *Instance) SetRegisterBuf(reg string, buffer []rune) {
// The user asked to paste a buffer onto the line, so we check from which register // The user asked to paste a buffer onto the line, so we check from which register
// we are supposed to select the buffer, and return it to the caller for insertion. // we are supposed to select the buffer, and return it to the caller for insertion.
func (rl *Instance) pasteFromRegister() (buffer []rune) { func (rl *Readline) pasteFromRegister() (buffer []rune) {
// When exiting this function the currently selected register is dropped, // When exiting this function the currently selected register is dropped,
defer rl.registers.resetRegister() defer rl.registers.resetRegister()
@ -155,7 +155,7 @@ func (rl *Instance) pasteFromRegister() (buffer []rune) {
return rl.GetFromRegister(activeRegister) return rl.GetFromRegister(activeRegister)
} }
func (rl *Instance) GetFromRegister(reg string) []rune { func (rl *Readline) GetFromRegister(reg string) []rune {
// Find the active register, and return its content. // Find the active register, and return its content.
num, err := strconv.Atoi(reg) num, err := strconv.Atoi(reg)
@ -264,7 +264,7 @@ func (r *registers) resetRegister() {
} }
// The user can show registers completions and insert, no matter the cursor position. // The user can show registers completions and insert, no matter the cursor position.
func (rl *Instance) completeRegisters() (groups []*CompletionGroup) { func (rl *Readline) completeRegisters() (groups []*CompletionGroup) {
// We set the info exceptionally // We set the info exceptionally
info := BLUE + "-- registers --" + RESET info := BLUE + "-- registers --" + RESET

View File

@ -2,7 +2,7 @@ package readline
// syntaxCompletion - applies syntax highlighting to the current input line. // syntaxCompletion - applies syntax highlighting to the current input line.
// nothing special to note here, nor any changes envisioned. // nothing special to note here, nor any changes envisioned.
func (rl *Instance) syntaxCompletion() { func (rl *Readline) syntaxCompletion() {
if rl.SyntaxCompleter == nil { if rl.SyntaxCompleter == nil {
return return
} }

View File

@ -2,13 +2,14 @@ package readline
import ( import (
"strings" "strings"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
// insertCandidateVirtual - When a completion candidate is selected, we insert it virtually in the input line: // insertCandidateVirtual - When a completion candidate is selected, we insert it virtually in the input line:
// this will not trigger further firltering against the other candidates. Each time this function // this will not trigger further firltering against the other candidates. Each time this function
// is called, any previous candidate is dropped, after being used for moving the cursor around. // is called, any previous candidate is dropped, after being used for moving the cursor around.
func (rl *Instance) insertCandidateVirtual(candidate []rune) { func (rl *Readline) insertCandidateVirtual(candidate []rune) {
for { for {
// I don't really understand why `0` is creaping in at the end of the // I don't really understand why `0` is creaping in at the end of the
// array but it only happens with unicode characters. // array but it only happens with unicode characters.
@ -57,7 +58,7 @@ func (rl *Instance) insertCandidateVirtual(candidate []rune) {
// Insert the current completion candidate into the input line. // Insert the current completion candidate into the input line.
// This candidate might either be the currently selected one (white frame), // This candidate might either be the currently selected one (white frame),
// or the only candidate available, if the total number of candidates is 1. // or the only candidate available, if the total number of candidates is 1.
func (rl *Instance) insertCandidate() { func (rl *Readline) insertCandidate() {
cur := rl.getCurrentGroup() cur := rl.getCurrentGroup()
@ -83,7 +84,7 @@ func (rl *Instance) insertCandidate() {
} }
// updateVirtualComp - Either insert the current completion candidate virtually, or on the real line. // updateVirtualComp - Either insert the current completion candidate virtually, or on the real line.
func (rl *Instance) updateVirtualComp() { func (rl *Readline) updateVirtualComp() {
cur := rl.getCurrentGroup() cur := rl.getCurrentGroup()
if cur != nil { if cur != nil {
@ -118,7 +119,7 @@ func (rl *Instance) updateVirtualComp() {
// resetVirtualComp - This function is called before most of our readline key handlers, // resetVirtualComp - This function is called before most of our readline key handlers,
// and makes sure that the current completion (virtually inserted) is either inserted or dropped, // and makes sure that the current completion (virtually inserted) is either inserted or dropped,
// and that all related parameters are reinitialized. // and that all related parameters are reinitialized.
func (rl *Instance) resetVirtualComp(drop bool) { func (rl *Readline) resetVirtualComp(drop bool) {
// If we don't have a current virtual completion, there's nothing to do. // If we don't have a current virtual completion, there's nothing to do.
// IMPORTANT: this MUST be first, to avoid nil problems with empty comps. // IMPORTANT: this MUST be first, to avoid nil problems with empty comps.
@ -196,7 +197,7 @@ func trimTrailing(comp string) (trimmed string, hadSlash bool) {
} }
// viDeleteByAdjustVirtual - Same as viDeleteByAdjust, but for our virtually completed input line. // viDeleteByAdjustVirtual - Same as viDeleteByAdjust, but for our virtually completed input line.
func (rl *Instance) viDeleteByAdjustVirtual(adjust int) { func (rl *Readline) viDeleteByAdjustVirtual(adjust int) {
var ( var (
newLine []rune newLine []rune
backOne bool backOne bool
@ -235,7 +236,7 @@ func (rl *Instance) viDeleteByAdjustVirtual(adjust int) {
} }
// viJumpEVirtual - Same as viJumpE, but for our virtually completed input line. // viJumpEVirtual - Same as viJumpE, but for our virtually completed input line.
func (rl *Instance) viJumpEVirtual(tokeniser func([]rune, int) ([]string, int, int)) (adjust int) { func (rl *Readline) viJumpEVirtual(tokeniser func([]rune, int) ([]string, int, int)) (adjust int) {
split, index, pos := tokeniser(rl.lineComp, rl.pos) split, index, pos := tokeniser(rl.lineComp, rl.pos)
if len(split) == 0 { if len(split) == 0 {
return return
@ -258,7 +259,7 @@ func (rl *Instance) viJumpEVirtual(tokeniser func([]rune, int) ([]string, int, i
return return
} }
func (rl *Instance) deleteVirtual() { func (rl *Readline) deleteVirtual() {
switch { switch {
case len(rl.lineComp) == 0: case len(rl.lineComp) == 0:
return return
@ -274,7 +275,7 @@ func (rl *Instance) deleteVirtual() {
// We are done with the current virtual completion candidate. // We are done with the current virtual completion candidate.
// Get ready for the next one // Get ready for the next one
func (rl *Instance) clearVirtualComp() { func (rl *Readline) clearVirtualComp() {
rl.line = rl.lineComp rl.line = rl.lineComp
rl.currentComp = []rune{} rl.currentComp = []rune{}
rl.compAddSpace = false rl.compAddSpace = false

View File

@ -28,7 +28,7 @@ const (
// getTabCompletion - This root function sets up all completion items and engines, // getTabCompletion - This root function sets up all completion items and engines,
// dealing with all search and completion modes. But it does not perform printing. // dealing with all search and completion modes. But it does not perform printing.
func (rl *Instance) getTabCompletion() { func (rl *Readline) getTabCompletion() {
// Populate registers if requested. // Populate registers if requested.
if rl.modeAutoFind && rl.searchMode == RegisterFind { if rl.modeAutoFind && rl.searchMode == RegisterFind {
@ -53,7 +53,7 @@ func (rl *Instance) getTabCompletion() {
} }
// getRegisterCompletion - Populates and sets up completion for Vim registers. // getRegisterCompletion - Populates and sets up completion for Vim registers.
func (rl *Instance) getRegisterCompletion() { func (rl *Readline) getRegisterCompletion() {
rl.tcGroups = rl.completeRegisters() rl.tcGroups = rl.completeRegisters()
if len(rl.tcGroups) == 0 { if len(rl.tcGroups) == 0 {
@ -84,7 +84,7 @@ func (rl *Instance) getRegisterCompletion() {
} }
// getTabSearchCompletion - Populates and sets up completion for completion search. // getTabSearchCompletion - Populates and sets up completion for completion search.
func (rl *Instance) getTabSearchCompletion() { func (rl *Readline) getTabSearchCompletion() {
// Get completions from the engine, and make sure there is a current group. // Get completions from the engine, and make sure there is a current group.
rl.getCompletions() rl.getCompletions()
@ -107,7 +107,7 @@ func (rl *Instance) getTabSearchCompletion() {
} }
// getHistorySearchCompletion - Populates and sets up completion for command history search // getHistorySearchCompletion - Populates and sets up completion for command history search
func (rl *Instance) getHistorySearchCompletion() { func (rl *Readline) getHistorySearchCompletion() {
// Refresh full list each time // Refresh full list each time
rl.tcGroups = rl.completeHistory() rl.tcGroups = rl.completeHistory()
@ -142,7 +142,7 @@ func (rl *Instance) getHistorySearchCompletion() {
// getNormalCompletion - Populates and sets up completion for normal comp mode. // getNormalCompletion - Populates and sets up completion for normal comp mode.
// Will automatically cancel the completion mode if there are no candidates. // Will automatically cancel the completion mode if there are no candidates.
func (rl *Instance) getNormalCompletion() { func (rl *Readline) getNormalCompletion() {
// Get completions groups, pass delayedTabContext and check nils // Get completions groups, pass delayedTabContext and check nils
rl.getCompletions() rl.getCompletions()
@ -172,7 +172,7 @@ func (rl *Instance) getNormalCompletion() {
// getCompletions - Calls the completion engine/function to yield a list of 0 or more completion groups, // getCompletions - Calls the completion engine/function to yield a list of 0 or more completion groups,
// sets up a delayed tab context and passes it on to the tab completion engine function, and ensure no // sets up a delayed tab context and passes it on to the tab completion engine function, and ensure no
// nil groups/items will pass through. This function is called by different comp search/nav modes. // nil groups/items will pass through. This function is called by different comp search/nav modes.
func (rl *Instance) getCompletions() { func (rl *Readline) getCompletions() {
// If there is no wired tab completion engine, nothing we can do. // If there is no wired tab completion engine, nothing we can do.
if rl.TabCompleter == nil { if rl.TabCompleter == nil {
@ -214,7 +214,7 @@ func (rl *Instance) getCompletions() {
// moveTabCompletionHighlight - This function is in charge of // moveTabCompletionHighlight - This function is in charge of
// computing the new position in the current completions liste. // computing the new position in the current completions liste.
func (rl *Instance) moveTabCompletionHighlight(x, y int) { func (rl *Readline) moveTabCompletionHighlight(x, y int) {
rl.completionOpen = true rl.completionOpen = true
g := rl.getCurrentGroup() g := rl.getCurrentGroup()
@ -253,7 +253,7 @@ func (rl *Instance) moveTabCompletionHighlight(x, y int) {
} }
// writeTabCompletion - Prints all completion groups and their items // writeTabCompletion - Prints all completion groups and their items
func (rl *Instance) writeTabCompletion() { func (rl *Readline) writeTabCompletion() {
// The final completions string to print. // The final completions string to print.
var completions string var completions string
@ -289,7 +289,7 @@ func (rl *Instance) writeTabCompletion() {
// cropCompletions - When the user cycles through a completion list longer // cropCompletions - When the user cycles through a completion list longer
// than the console MaxTabCompleterRows value, we crop the completions string // than the console MaxTabCompleterRows value, we crop the completions string
// so that "global" cycling (across all groups) is printed correctly. // so that "global" cycling (across all groups) is printed correctly.
func (rl *Instance) cropCompletions(comps string) (cropped string, usedY int) { func (rl *Readline) cropCompletions(comps string) (cropped string, usedY int) {
// If we actually fit into the MaxTabCompleterRows, return the comps // If we actually fit into the MaxTabCompleterRows, return the comps
if rl.tcUsedY < rl.MaxTabCompleterRows { if rl.tcUsedY < rl.MaxTabCompleterRows {
@ -366,7 +366,7 @@ func (rl *Instance) cropCompletions(comps string) (cropped string, usedY int) {
return return
} }
func (rl *Instance) getAbsPos() int { func (rl *Readline) getAbsPos() int {
var prev int var prev int
var foundCurrent bool var foundCurrent bool
for _, grp := range rl.tcGroups { for _, grp := range rl.tcGroups {
@ -390,7 +390,7 @@ func (rl *Instance) getAbsPos() int {
// We pass a special subset of the current input line, so that // We pass a special subset of the current input line, so that
// completions are available no matter where the cursor is. // completions are available no matter where the cursor is.
func (rl *Instance) getCompletionLine() (line []rune, pos int) { func (rl *Readline) getCompletionLine() (line []rune, pos int) {
pos = rl.pos - len(rl.currentComp) pos = rl.pos - len(rl.currentComp)
if pos < 0 { if pos < 0 {
@ -409,7 +409,7 @@ func (rl *Instance) getCompletionLine() (line []rune, pos int) {
return return
} }
func (rl *Instance) getCurrentGroup() (group *CompletionGroup) { func (rl *Readline) getCurrentGroup() (group *CompletionGroup) {
for _, g := range rl.tcGroups { for _, g := range rl.tcGroups {
if g.isCurrent && len(g.Suggestions) > 0 { if g.isCurrent && len(g.Suggestions) > 0 {
return g return g
@ -431,7 +431,7 @@ func (rl *Instance) getCurrentGroup() (group *CompletionGroup) {
// cycleNextGroup - Finds either the first non-empty group, // cycleNextGroup - Finds either the first non-empty group,
// or the next non-empty group after the current one. // or the next non-empty group after the current one.
func (rl *Instance) cycleNextGroup() { func (rl *Readline) cycleNextGroup() {
for i, g := range rl.tcGroups { for i, g := range rl.tcGroups {
if g.isCurrent { if g.isCurrent {
g.isCurrent = false g.isCurrent = false
@ -452,7 +452,7 @@ func (rl *Instance) cycleNextGroup() {
} }
// cyclePreviousGroup - Same as cycleNextGroup but reverse // cyclePreviousGroup - Same as cycleNextGroup but reverse
func (rl *Instance) cyclePreviousGroup() { func (rl *Readline) cyclePreviousGroup() {
for i, g := range rl.tcGroups { for i, g := range rl.tcGroups {
if g.isCurrent { if g.isCurrent {
g.isCurrent = false g.isCurrent = false
@ -471,7 +471,7 @@ func (rl *Instance) cyclePreviousGroup() {
} }
// Check if we have a single completion candidate // Check if we have a single completion candidate
func (rl *Instance) hasOneCandidate() bool { func (rl *Readline) hasOneCandidate() bool {
if len(rl.tcGroups) == 0 { if len(rl.tcGroups) == 0 {
return false return false
} }
@ -509,7 +509,7 @@ func (rl *Instance) hasOneCandidate() bool {
// - The user-specified max completion length // - The user-specified max completion length
// - The terminal lengh // - The terminal lengh
// we use this function to prompt for confirmation before printing comps. // we use this function to prompt for confirmation before printing comps.
func (rl *Instance) promptCompletionConfirm(sentence string) { func (rl *Readline) promptCompletionConfirm(sentence string) {
rl.infoText = []rune(sentence) rl.infoText = []rune(sentence)
rl.compConfirmWait = true rl.compConfirmWait = true
@ -518,7 +518,7 @@ func (rl *Instance) promptCompletionConfirm(sentence string) {
rl.renderHelpers() rl.renderHelpers()
} }
func (rl *Instance) getCompletionCount() (comps int, lines int, adjusted int) { func (rl *Readline) getCompletionCount() (comps int, lines int, adjusted int) {
for _, group := range rl.tcGroups { for _, group := range rl.tcGroups {
comps += len(group.Suggestions) comps += len(group.Suggestions)
// if group.Name != "" { // if group.Name != "" {
@ -535,7 +535,7 @@ func (rl *Instance) getCompletionCount() (comps int, lines int, adjusted int) {
return return
} }
func (rl *Instance) resetTabCompletion() { func (rl *Readline) resetTabCompletion() {
rl.modeTabCompletion = false rl.modeTabCompletion = false
rl.tabCompletionSelect = false rl.tabCompletionSelect = false
rl.compConfirmWait = false rl.compConfirmWait = false

View File

@ -12,7 +12,7 @@ const (
RegisterFind RegisterFind
) )
func (rl *Instance) backspaceTabFind() { func (rl *Readline) backspaceTabFind() {
if len(rl.tfLine) > 0 { if len(rl.tfLine) > 0 {
rl.tfLine = rl.tfLine[:len(rl.tfLine)-1] rl.tfLine = rl.tfLine[:len(rl.tfLine)-1]
} }
@ -21,7 +21,7 @@ func (rl *Instance) backspaceTabFind() {
// Filter and refresh (print) a list of completions. The caller should have reset // Filter and refresh (print) a list of completions. The caller should have reset
// the virtual completion system before, so that should not clash with this. // the virtual completion system before, so that should not clash with this.
func (rl *Instance) updateTabFind(r []rune) { func (rl *Readline) updateTabFind(r []rune) {
rl.tfLine = append(rl.tfLine, r...) rl.tfLine = append(rl.tfLine, r...)
@ -34,7 +34,7 @@ func (rl *Instance) updateTabFind(r []rune) {
rl.renderHelpers() rl.renderHelpers()
} }
func (rl *Instance) resetTabFind() { func (rl *Readline) resetTabFind() {
rl.modeTabFind = false rl.modeTabFind = false
// rl.modeAutoFind = false // Added, because otherwise it gets stuck on search completions // rl.modeAutoFind = false // Added, because otherwise it gets stuck on search completions

View File

@ -7,12 +7,12 @@ import (
// DelayedTabContext is a custom context interface for async updates to the tab completions // DelayedTabContext is a custom context interface for async updates to the tab completions
type DelayedTabContext struct { type DelayedTabContext struct {
rl *Instance rl *Readline
Context context.Context Context context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
func delayedSyntaxTimer(rl *Instance, i int64) { func delayedSyntaxTimer(rl *Readline, i int64) {
if rl.PasswordMask != 0 || rl.DelayedSyntaxWorker == nil { if rl.PasswordMask != 0 || rl.DelayedSyntaxWorker == nil {
return return
} }

View File

@ -5,7 +5,7 @@ type undoItem struct {
pos int pos int
} }
func (rl *Instance) undoAppendHistory() { func (rl *Readline) undoAppendHistory() {
defer func() { rl.viUndoSkipAppend = false }() defer func() { rl.viUndoSkipAppend = false }()
if rl.viUndoSkipAppend { if rl.viUndoSkipAppend {
@ -18,7 +18,7 @@ func (rl *Instance) undoAppendHistory() {
}) })
} }
func (rl *Instance) undoLast() { func (rl *Readline) undoLast() {
var undo undoItem var undo undoItem
for { for {
if len(rl.viUndoHistory) == 0 { if len(rl.viUndoHistory) == 0 {

View File

@ -10,7 +10,7 @@ import (
// updateHelpers is a key part of the whole refresh process: // updateHelpers is a key part of the whole refresh process:
// it should coordinate reprinting the input line, any Infos and completions // it should coordinate reprinting the input line, any Infos and completions
// and manage to get back to the current (computed) cursor coordinates // and manage to get back to the current (computed) cursor coordinates
func (rl *Instance) updateHelpers() { func (rl *Readline) updateHelpers() {
print(seqHideCursor) print(seqHideCursor)
// Load all Infos & completions before anything. // Load all Infos & completions before anything.
// Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText() // Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText()
@ -19,7 +19,9 @@ func (rl *Instance) updateHelpers() {
if rl.modeTabCompletion && !rl.completionOpen { if rl.modeTabCompletion && !rl.completionOpen {
rl.getTabCompletion() rl.getTabCompletion()
} else { } else {
if rl.completionOpen { rl.completionOpen = false } if rl.completionOpen {
rl.completionOpen = false
}
} }
// We clear everything // We clear everything
@ -49,7 +51,7 @@ func getWidth(x []rune) int {
} }
// Update reference should be called only once in a "loop" (not Readline(), but key control loop) // Update reference should be called only once in a "loop" (not Readline(), but key control loop)
func (rl *Instance) updateReferences() { func (rl *Readline) updateReferences() {
// We always need to work with clean data, // We always need to work with clean data,
// since we will have incrementers all around // since we will have incrementers all around
@ -103,7 +105,7 @@ func (rl *Instance) updateReferences() {
} }
} }
func (rl *Instance) resetHelpers() { func (rl *Readline) resetHelpers() {
rl.modeAutoFind = false rl.modeAutoFind = false
// Now reset all below-input helpers // Now reset all below-input helpers
@ -113,7 +115,7 @@ func (rl *Instance) resetHelpers() {
// clearHelpers - Clears everything: prompt, input, Infos & comps, // clearHelpers - Clears everything: prompt, input, Infos & comps,
// and comes back at the prompt. // and comes back at the prompt.
func (rl *Instance) clearHelpers() { func (rl *Readline) clearHelpers() {
// Now go down to the last line of input // Now go down to the last line of input
moveCursorDown(rl.fullY - rl.posY) moveCursorDown(rl.fullY - rl.posY)
@ -132,7 +134,7 @@ func (rl *Instance) clearHelpers() {
// renderHelpers - pritns all components (prompt, line, Infos & comps) // renderHelpers - pritns all components (prompt, line, Infos & comps)
// and replaces the cursor to its current position. This function never // and replaces the cursor to its current position. This function never
// computes or refreshes any value, except from inside the echo function. // computes or refreshes any value, except from inside the echo function.
func (rl *Instance) renderHelpers() { func (rl *Readline) renderHelpers() {
// when the instance is in this state we want it to be "below" the user's // when the instance is in this state we want it to be "below" the user's
// input for it to be aligned properly // input for it to be aligned properly
@ -197,14 +199,14 @@ func (rl *Instance) renderHelpers() {
moveCursorForwards(rl.posX) moveCursorForwards(rl.posX)
} }
func (rl *Instance) bufprintF(format string, a ...any) { func (rl *Readline) bufprintF(format string, a ...any) {
fmt.Fprintf(rl.bufferedOut, format, a...) fmt.Fprintf(rl.bufferedOut, format, a...)
} }
func (rl *Instance) bufprint(text string) { func (rl *Readline) bufprint(text string) {
fmt.Fprint(rl.bufferedOut, text) fmt.Fprint(rl.bufferedOut, text)
} }
func (rl *Instance) bufflush() { func (rl *Readline) bufflush() {
rl.bufferedOut.Flush() rl.bufferedOut.Flush()
} }

View File

@ -34,6 +34,7 @@ var (
) )
type ViAction int type ViAction int
const ( const (
VimActionYank = iota VimActionYank = iota
VimActionPaste VimActionPaste
@ -52,7 +53,7 @@ var (
// vi - Apply a key to a Vi action. Note that as in the rest of the code, all cursor movements // vi - Apply a key to a Vi action. Note that as in the rest of the code, all cursor movements
// have been moved away, and only the rl.pos is adjusted: when echoing the input line, the shell // have been moved away, and only the rl.pos is adjusted: when echoing the input line, the shell
// will compute the new cursor pos accordingly. // will compute the new cursor pos accordingly.
func (rl *Instance) vi(r rune) { func (rl *Readline) vi(r rune) {
activeRegister := string(rl.registers.currentRegister) activeRegister := string(rl.registers.currentRegister)
// Check if we are in register mode. If yes, and for some characters, // Check if we are in register mode. If yes, and for some characters,
@ -384,7 +385,7 @@ func (rl *Instance) vi(r rune) {
} }
} }
func (rl *Instance) getViIterations() int { func (rl *Readline) getViIterations() int {
i, _ := strconv.Atoi(rl.viIteration) i, _ := strconv.Atoi(rl.viIteration)
if i < 1 { if i < 1 {
i = 1 i = 1
@ -393,7 +394,7 @@ func (rl *Instance) getViIterations() int {
return i return i
} }
func (rl *Instance) refreshVimStatus() { func (rl *Readline) refreshVimStatus() {
rl.ViModeCallback(rl.modeViMode) rl.ViModeCallback(rl.modeViMode)
rl.computePrompt() rl.computePrompt()
rl.updateHelpers() rl.updateHelpers()
@ -401,7 +402,7 @@ func (rl *Instance) refreshVimStatus() {
// viInfoMessage - lmorg's way of showing Vim status is to overwrite the info. // viInfoMessage - lmorg's way of showing Vim status is to overwrite the info.
// Currently not used, as there is a possibility to show the current Vim mode in the prompt. // Currently not used, as there is a possibility to show the current Vim mode in the prompt.
func (rl *Instance) viInfoMessage() { func (rl *Readline) viInfoMessage() {
switch rl.modeViMode { switch rl.modeViMode {
case VimKeys: case VimKeys:
rl.infoText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)") rl.infoText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
@ -421,7 +422,7 @@ func (rl *Instance) viInfoMessage() {
rl.renderHelpers() rl.renderHelpers()
} }
func (rl *Instance) viJumpB(tokeniser tokeniser) (adjust int) { func (rl *Readline) viJumpB(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos) split, index, pos := tokeniser(rl.line, rl.pos)
switch { switch {
case len(split) == 0: case len(split) == 0:
@ -436,7 +437,7 @@ func (rl *Instance) viJumpB(tokeniser tokeniser) (adjust int) {
return adjust * -1 return adjust * -1
} }
func (rl *Instance) viJumpE(tokeniser tokeniser) (adjust int) { func (rl *Readline) viJumpE(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos) split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 { if len(split) == 0 {
return return
@ -459,7 +460,7 @@ func (rl *Instance) viJumpE(tokeniser tokeniser) (adjust int) {
return return
} }
func (rl *Instance) viJumpW(tokeniser tokeniser) (adjust int) { func (rl *Readline) viJumpW(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos) split, index, pos := tokeniser(rl.line, rl.pos)
switch { switch {
case len(split) == 0: case len(split) == 0:
@ -472,7 +473,7 @@ func (rl *Instance) viJumpW(tokeniser tokeniser) (adjust int) {
return return
} }
func (rl *Instance) viJumpPreviousBrace() (adjust int) { func (rl *Readline) viJumpPreviousBrace() (adjust int) {
if rl.pos == 0 { if rl.pos == 0 {
return 0 return 0
} }
@ -486,7 +487,7 @@ func (rl *Instance) viJumpPreviousBrace() (adjust int) {
return 0 return 0
} }
func (rl *Instance) viJumpNextBrace() (adjust int) { func (rl *Readline) viJumpNextBrace() (adjust int) {
if rl.pos >= len(rl.line)-1 { if rl.pos >= len(rl.line)-1 {
return 0 return 0
} }
@ -500,7 +501,7 @@ func (rl *Instance) viJumpNextBrace() (adjust int) {
return 0 return 0
} }
func (rl *Instance) viJumpBracket() (adjust int) { func (rl *Readline) viJumpBracket() (adjust int) {
split, index, pos := tokeniseBrackets(rl.line, rl.pos) split, index, pos := tokeniseBrackets(rl.line, rl.pos)
switch { switch {
case len(split) == 0: case len(split) == 0:

View File

@ -5,7 +5,7 @@ import (
) )
// vimDelete - // vimDelete -
func (rl *Instance) viDelete(r rune) { func (rl *Readline) viDelete(r rune) {
// We are allowed to type iterations after a delete ('d') command. // We are allowed to type iterations after a delete ('d') command.
// in which case we don't exit the delete mode. The next thing typed // in which case we don't exit the delete mode. The next thing typed
@ -91,7 +91,7 @@ func (rl *Instance) viDelete(r rune) {
} }
} }
func (rl *Instance) viDeleteByAdjust(adjust int) { func (rl *Readline) viDeleteByAdjust(adjust int) {
var ( var (
newLine []rune newLine []rune
backOne bool backOne bool
@ -142,7 +142,11 @@ func (rl *Instance) viDeleteByAdjust(adjust int) {
rl.updateHelpers() rl.updateHelpers()
} }
func (rl *Instance) vimDeleteToken(r rune) bool { func (rl *Readline) DeleteByAmount(adjust int) {
rl.viDeleteByAdjust(adjust)
}
func (rl *Readline) vimDeleteToken(r rune) bool {
tokens, _, _ := tokeniseSplitSpaces(rl.line, 0) tokens, _, _ := tokeniseSplitSpaces(rl.line, 0)
pos := int(r) - 48 // convert ASCII to integer pos := int(r) - 48 // convert ASCII to integer
if pos > len(tokens) { if pos > len(tokens) {

221
golibs/snail/lua.go Normal file
View File

@ -0,0 +1,221 @@
// 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())
}

304
golibs/snail/snail.go Normal file
View File

@ -0,0 +1,304 @@
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()
}

147
golibs/yarn/yarn.go Normal file
View File

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

6
job.go
View File

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

View File

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

View File

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

100
lua.go
View File

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

38
main.go
View File

@ -21,7 +21,6 @@ import (
"github.com/pborman/getopt" "github.com/pborman/getopt"
"github.com/maxlandon/readline" "github.com/maxlandon/readline"
"golang.org/x/term" "golang.org/x/term"
"mvdan.cc/sh/v3/interp"
) )
var ( var (
@ -38,16 +37,27 @@ var (
cmds *commander.Commander cmds *commander.Commander
defaultConfPath string defaultConfPath string
defaultHistPath string defaultHistPath string
runner *interp.Runner
) )
func main() { func main() {
runner, _ = interp.New() 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() curuser, _ = user.Current()
homedir := curuser.HomeDir
confDir, _ = os.UserConfigDir() 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 // i honestly dont know what directories to use for this
switch runtime.GOOS { switch runtime.GOOS {
@ -141,10 +151,11 @@ func main() {
confpath := ".hilbishrc.lua" confpath := ".hilbishrc.lua"
if err != nil { if err != nil {
// If it wasnt found, go to the real sample conf // If it wasnt found, go to the real sample conf
_, err = os.ReadFile(sampleConfPath) sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua")
confpath = sampleConfPath _, err = os.ReadFile(sampleConfigPath)
confpath = sampleConfigPath
if err != nil { if err != nil {
fmt.Println("could not find .hilbishrc.lua or", sampleConfPath) fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath)
return return
} }
} }
@ -313,15 +324,6 @@ func removeDupes(slice []string) []string {
return newSlice 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) { func exit(code int) {
jobs.stopAll() jobs.stopAll()

61
nature/abbr.lua Normal file
View File

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

View File

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

View File

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

View File

@ -1,13 +1,25 @@
-- @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 lunacolors = require 'lunacolors'
local M = {} local doc = {}
function M.highlight(text) --- Performs basic Lua code highlighting.
--- @param text string Code/text to do highlighting on.
function doc.highlight(text)
return text:gsub('\'.-\'', lunacolors.yellow) return text:gsub('\'.-\'', lunacolors.yellow)
--:gsub('%-%- .-', lunacolors.black) --:gsub('%-%- .-', lunacolors.black)
end end
function M.renderCodeBlock(text) --- 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)
local longest = 0 local longest = 0
local lines = string.split(text:gsub('\t', ' '), '\n') local lines = string.split(text:gsub('\t', ' '), '\n')
@ -17,14 +29,18 @@ function M.renderCodeBlock(text)
end end
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
lines[i] = lunacolors.format('{greyBg}' .. ' ' .. M.highlight(line:sub(0, longest)) lines[i] = lunacolors.format('{greyBg}' .. ' ' .. doc.highlight(line:sub(0, longest))
.. string.rep(' ', longest - line:len()) .. ' ') .. string.rep(' ', longest - line:len()) .. ' ')
end end
return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n' return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
end end
function M.renderInfoBlock(type, text) --- 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)
local longest = 0 local longest = 0
local lines = string.split(text:gsub('\t', ' '), '\n') local lines = string.split(text:gsub('\t', ' '), '\n')
@ -34,7 +50,7 @@ function M.renderInfoBlock(type, text)
end end
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
lines[i] = ' ' .. M.highlight(line:sub(0, longest)) lines[i] = ' ' .. doc.highlight(line:sub(0, longest))
.. string.rep(' ', longest - line:len()) .. ' ' .. string.rep(' ', longest - line:len()) .. ' '
end end
@ -44,4 +60,4 @@ function M.renderInfoBlock(type, text)
end end
return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n' return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n'
end end
return M return doc

28
nature/editor.lua Normal file
View File

@ -0,0 +1,28 @@
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)

10
nature/env.lua Normal file
View File

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

View File

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

View File

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

79
nature/hilbish.lua Normal file
View File

@ -0,0 +1,79 @@
-- @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,3 +1,14 @@
-- @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 bait = require 'bait'
local commander = require 'commander' local commander = require 'commander'
local lunacolors = require 'lunacolors' local lunacolors = require 'lunacolors'
@ -44,6 +55,8 @@ function hilbish.messages.send(message)
bait.throw('hilbish.notification', message) bait.throw('hilbish.notification', message)
end end
--- Marks a message at `idx` as read.
--- @param idx number
function hilbish.messages.read(idx) function hilbish.messages.read(idx)
local msg = M._messages[idx] local msg = M._messages[idx]
if msg then if msg then
@ -52,16 +65,20 @@ function hilbish.messages.read(idx)
end end
end end
function hilbish.messages.readAll(idx) --- Marks all messages as read.
function hilbish.messages.readAll()
for _, msg in ipairs(hilbish.messages.all()) do for _, msg in ipairs(hilbish.messages.all()) do
hilbish.messages.read(msg.index) hilbish.messages.read(msg.index)
end end
end end
--- Returns the amount of unread messages.
function hilbish.messages.unreadCount() function hilbish.messages.unreadCount()
return unread return unread
end end
--- Deletes the message at `idx`.
--- @param idx number
function hilbish.messages.delete(idx) function hilbish.messages.delete(idx)
local msg = M._messages[idx] local msg = M._messages[idx]
if not msg then if not msg then
@ -71,12 +88,14 @@ function hilbish.messages.delete(idx)
M._messages[idx] = nil M._messages[idx] = nil
end end
--- Deletes all messages.
function hilbish.messages.clear() function hilbish.messages.clear()
for _, msg in ipairs(hilbish.messages.all()) do for _, msg in ipairs(hilbish.messages.all()) do
hilbish.messages.delete(msg.index) hilbish.messages.delete(msg.index)
end end
end end
--- Returns all messages.
function hilbish.messages.all() function hilbish.messages.all()
return M._messages return M._messages
end end

View File

@ -18,12 +18,18 @@ table.insert(package.searchers, function(module)
return function() return hilbish.module.load(path) end, path return function() return hilbish.module.load(path) end, path
end) end)
require 'nature.hilbish'
require 'nature.processors'
require 'nature.commands' require 'nature.commands'
require 'nature.completions' require 'nature.completions'
require 'nature.opts' require 'nature.opts'
require 'nature.vim' require 'nature.vim'
require 'nature.runner' require 'nature.runner'
require 'nature.hummingbird' require 'nature.hummingbird'
require 'nature.env'
require 'nature.abbr'
require 'nature.editor'
local shlvl = tonumber(os.getenv 'SHLVL') local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then if shlvl ~= nil then
@ -32,36 +38,6 @@ else
os.setenv('SHLVL', '0') os.setenv('SHLVL', '0')
end 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 do
local startSearchPath = hilbish.userDir.data .. '/hilbish/start/?/init.lua;' local startSearchPath = hilbish.userDir.data .. '/hilbish/start/?/init.lua;'
.. hilbish.userDir.data .. '/hilbish/start/?.lua' .. hilbish.userDir.data .. '/hilbish/start/?.lua'

View File

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

View File

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

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