Compare commits

..

20 Commits

Author SHA1 Message Date
TorchedSammy 3460df6863 fix: make cd only throw command.exit hooks 2021-04-05 18:31:59 -04:00
TorchedSammy fad4282345 chore: bump version 2021-04-05 18:25:55 -04:00
TorchedSammy febd6ba2cd chore: merge from master 2021-04-05 18:19:08 -04:00
TorchedSammy 84d55a38b0 feat: change multiline prompt via multiprompt function (closes #13) 2021-04-05 18:09:21 -04:00
TorchedSammy 4c63009f56 feat!: add more functions to ansikit, change text function to format 2021-04-05 17:35:43 -04:00
TorchedSammy 898f8816ff fix: update to work with latest hilbish 2021-04-05 17:27:55 -04:00
TorchedSammy 271ea946eb fix: update to work with latest ansikit 2021-04-05 17:26:55 -04:00
TorchedSammy 807ec15faa fix: cleanup and move exit command to lua side 2021-04-05 15:21:44 -04:00
TorchedSammy ef4975f984 feat: fallback to default config on error loading user conf 2021-04-04 20:31:32 -04:00
TorchedSammy 3cb3b34023 fix: add back sh, cleanup a bit and use new functions 2021-04-04 20:30:47 -04:00
TorchedSammy 22270dc537 fix: trim input and remove comment 2021-04-04 20:30:03 -04:00
TorchedSammy cfdab00684 fix: make shell continue prompt "sh> " 2021-04-04 18:46:37 -04:00
TorchedSammy cc183620c5 fix: move input checks to main.go, some cleanup 2021-04-04 18:42:56 -04:00
TorchedSammy a30489aa52 chore: merge from master 2021-04-04 13:15:55 -04:00
TorchedSammy f4b45b370e feat: multiline input with \ (hilbish only has lua input currently) 2021-04-04 13:04:57 -04:00
Jack 6c050194ed
feat: Proxy global string variables to ENV (#16) 2021-04-03 16:08:04 -04:00
Jack ad1be6b5f5
feat: Add command.exit signal to bait (#15) 2021-04-03 14:33:38 -04:00
TorchedSammy 71504c9990 chore: add a "motd" 2021-04-03 13:14:25 -04:00
TorchedSammy 4f4237a3e9 chore: split up go source files 2021-04-03 13:13:45 -04:00
TorchedSammy a0bc8e026a feat: save history to file 2021-04-01 22:12:03 -04:00
6 changed files with 388 additions and 184 deletions

View File

@ -3,19 +3,19 @@ ansikit = require 'ansikit'
bait = require 'bait' bait = require 'bait'
function doPrompt(fail) function doPrompt(fail)
prompt(ansikit.text( prompt(ansikit.format(
'{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆{reset} ' '{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆{reset} '
)) ))
end end
print(ansikit.format('Welcome {cyan}'.. os.getenv 'USER' ..
'{reset} to {magenta}Hilbish{reset},\n' ..
'the nice lil shell for {blue}Lua{reset} fanatics!\n'))
doPrompt() doPrompt()
bait.catch('command.fail', function() bait.catch('command.exit', function(code)
doPrompt(true) doPrompt(code ~= 0)
end)
bait.catch('command.success', function()
doPrompt()
end) end)
--hook("tab complete", function ()) --hook("tab complete", function ())

View File

@ -4,13 +4,78 @@
local ansikit = {} local ansikit = {}
ansikit.getCSI = function (code, endc) ansikit.clear = function(scrollback)
typ = (scrollback and 3 or 2)
return ansikit.printCSI(typ, 'J')
end
ansikit.clearFromPos = function(scrollback)
return ansikit.printCSI(0, 'J')
end
ansikit.clearLine = function()
return ansikit.printCSI(2, 'K')
end
ansikit.clearToPos = function()
return ansikit.printCSI(1, 'J')
end
ansikit.color256 = function(color)
color = (color and color or 0)
return ansikit.printCSI('38;5;' .. color)
end
ansikit.cursorDown = function(y)
y = (y and y or 1)
return ansikit.printCSI(y, 'B')
end
ansikit.cursorLeft = function(x)
x = (x and x or 1)
return ansikit.printCSI(x, 'D')
end
-- TODO: cursorPos
-- https://github.com/Luvella/AnsiKit/blob/master/lib/index.js#L90
ansikit.cursorRight = function(x)
x = (x and x or 1)
return ansikit.printCSI(x, 'C')
end
ansikit.cursorStyle = function(style)
style = (style and style or ansikit.underlineCursor)
if style > 6 or style < 1 then style = ansikit.underlineCursor end
return ansikit.printCSI(style, ' q')
end
ansikit.cursorTo = function(x, y)
x, y = (x and x or 1), (y and y or 1)
return ansikit.printCSI(x .. ';' .. y, 'H')
end
ansikit.cursorUp = function(y)
y = (y and y or 1)
return ansikit.printCSI(y, 'A')
end
ansikit.getCode = function(code, terminate)
endc = (endc and endc or 'm')
return string.char(0x001b) .. code ..
(terminate and string.char(0x001b) .. '\\' or '')
end
ansikit.getCSI = function(code, endc)
endc = (endc and endc or 'm') endc = (endc and endc or 'm')
return string.char(0x001b) .. '[' .. code .. endc return string.char(0x001b) .. '[' .. code .. endc
end end
ansikit.text = function (text) ansikit.format = function(text)
local colors = { local colors = {
-- TODO: write codes manually instead of using functions
-- less function calls = faster ????????
reset = {'{reset}', ansikit.getCSI(0)}, reset = {'{reset}', ansikit.getCSI(0)},
bold = {'{bold}', ansikit.getCSI(1)}, bold = {'{bold}', ansikit.getCSI(1)},
dim = {'{dim}', ansikit.getCSI(2)}, dim = {'{dim}', ansikit.getCSI(2)},
@ -25,8 +90,22 @@ ansikit.text = function (text)
yellow = {'{yellow}', ansikit.getCSI(33)}, yellow = {'{yellow}', ansikit.getCSI(33)},
blue = {'{blue}', ansikit.getCSI(34)}, blue = {'{blue}', ansikit.getCSI(34)},
magenta = {'{magenta}', ansikit.getCSI(35)}, magenta = {'{magenta}', ansikit.getCSI(35)},
cyan = {'{cyan}', ansikit.getCSI(36)} cyan = {'{cyan}', ansikit.getCSI(36)},
-- TODO: Background, bright colors white = {'{white}', ansikit.getCSI(37)},
red_bg = {'{red-bg}', ansikit.getCSI(41)},
green_bg = {'{green-bg}', ansikit.getCSI(42)},
yellow_bg = {'{green-bg}', ansikit.getCSI(43)},
blue_bg = {'{blue-bg}', ansikit.getCSI(44)},
magenta_bg = {'{magenta-bg}', ansikit.getCSI(45)},
cyan_bg = {'{cyan-bg}', ansikit.getCSI(46)},
white_bg = {'{white-bg}', ansikit.getCSI(47)},
gray = {'{gray}', ansikit.getCSI(90)},
bright_red = {'{bright-red}', ansikit.getCSI(91)},
bright_green = {'{bright-green}', ansikit.getCSI(92)},
bright_yellow = {'{bright-yellow}', ansikit.getCSI(93)},
bright_blue = {'{bright-blue}', ansikit.getCSI(94)},
bright_magenta = {'{bright-magenta}', ansikit.getCSI(95)},
bright_cyan = {'{bright-cyan}', ansikit.getCSI(96)}
} }
for k, v in pairs(colors) do for k, v in pairs(colors) do
@ -36,5 +115,25 @@ ansikit.text = function (text)
return text return text
end end
ansikit.print = function(text)
io.write(ansikit.format(text))
return ansikit
end
ansikit.printCode = function(code, terminate)
io.write(ansikit.getCode(code, terminate))
return ansikit
end
ansikit.printCSI = function(code, endc)
io.write(ansikit.getCSI(code, endc))
return ansikit
end
ansikit.println = function(text)
print(ansikit.print(text))
return ansikit
end
return ansikit return ansikit

21
lua.go
View File

@ -10,12 +10,22 @@ import (
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
) )
var minimalconf = `
ansikit = require 'ansikit'
prompt(ansikit.format(
'{blue}%u {cyan}%d {green}{reset} '
))
`
func LuaInit() { func LuaInit() {
l = lua.NewState() l = lua.NewState()
l.OpenLibs() l.OpenLibs()
l.SetGlobal("_ver", lua.LString(version))
l.SetGlobal("prompt", l.NewFunction(hshprompt)) l.SetGlobal("prompt", l.NewFunction(hshprompt))
l.SetGlobal("multiprompt", l.NewFunction(hshmlprompt))
l.SetGlobal("alias", l.NewFunction(hshalias)) l.SetGlobal("alias", l.NewFunction(hshalias))
// Add fs module to Lua // Add fs module to Lua
@ -54,7 +64,10 @@ func LuaInit() {
// Run config // Run config
err = l.DoFile(homedir + "/.hilbishrc.lua") err = l.DoFile(homedir + "/.hilbishrc.lua")
if err != nil { if err != nil {
panic(err) fmt.Fprintln(os.Stderr, err,
"\nAn error has occured while loading your config! Falling back to minimal default config.\n")
l.DoString(minimalconf)
} }
} }
@ -64,6 +77,12 @@ func hshprompt(L *lua.LState) int {
return 0 return 0
} }
func hshmlprompt(L *lua.LState) int {
multilinePrompt = L.ToString(1)
return 0
}
func hshalias(L *lua.LState) int { func hshalias(L *lua.LState) int {
alias := L.ToString(1) alias := L.ToString(1)
source := L.ToString(2) source := L.ToString(2)

200
main.go
View File

@ -1,35 +1,34 @@
package main package main
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"os/user" "os/user"
"syscall" "syscall"
"os/signal" "os/signal"
"strings" "strings"
"bufio"
"io" "io"
"context"
hooks "hilbish/golibs/bait" hooks "hilbish/golibs/bait"
"github.com/akamensky/argparse" "github.com/akamensky/argparse"
"github.com/bobappleyard/readline" "github.com/bobappleyard/readline"
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
"layeh.com/gopher-luar"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
) )
const version = "0.2.0" const version = "0.3.0"
var l *lua.LState var l *lua.LState
// User's prompt, this will get set when lua side is initialized // User's prompt, this will get set when lua side is initialized
var prompt string var prompt string
var multilinePrompt = "> "
// Map of builtin/custom commands defined in the commander lua module // Map of builtin/custom commands defined in the commander lua module
var commands = map[string]bool{} var commands = map[string]bool{}
// Command aliases // Command aliases
var aliases = map[string]string{} var aliases = map[string]string{}
var bait hooks.Bait var bait hooks.Bait
var homedir string
func main() { func main() {
parser := argparse.NewParser("hilbish", "A shell for lua and flower lovers") parser := argparse.NewParser("hilbish", "A shell for lua and flower lovers")
@ -58,7 +57,7 @@ func main() {
// Set $SHELL if the user wants to // Set $SHELL if the user wants to
if *setshflag { os.Setenv("SHELL", os.Args[0]) } if *setshflag { os.Setenv("SHELL", os.Args[0]) }
homedir, _ := os.UserHomeDir() homedir, _ = os.UserHomeDir()
// If user's config doesn't exixt, // If user's config doesn't exixt,
if _, err := os.Stat(homedir + "/.hilbishrc.lua"); os.IsNotExist(err) { if _, err := os.Stat(homedir + "/.hilbishrc.lua"); os.IsNotExist(err) {
// Read default from current directory // Read default from current directory
@ -87,9 +86,10 @@ func main() {
LuaInit() LuaInit()
readline.Completer = readline.FilenameCompleter readline.Completer = readline.FilenameCompleter
readline.LoadHistory(homedir + "/.hilbish-history")
for { for {
cmdString, err := readline.String(fmtPrompt()) input, err := readline.String(fmtPrompt())
if err == io.EOF { if err == io.EOF {
// Exit if user presses ^D (ctrl + d) // Exit if user presses ^D (ctrl + d)
fmt.Println("") fmt.Println("")
@ -100,80 +100,36 @@ func main() {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
} }
// I have no idea if we need this anymore input = strings.TrimSpace(input)
cmdString = strings.TrimSuffix(cmdString, "\n") if len(input) == 0 { continue }
// First try to run user input in Lua
err = l.DoString(cmdString)
if err == nil { if strings.HasSuffix(input, "\\") {
// If it succeeds, add to history and prompt again for {
readline.AddHistory(cmdString) input, err = ContinuePrompt(strings.TrimSuffix(input, "\\"))
bait.Em.Emit("command.success", nil)
continue
}
// Split up the input if err != nil || !strings.HasSuffix(input, "\\") { break }
cmdArgs, cmdString := splitInput(cmdString)
// If there's actually no input, prompt again
if len(cmdArgs) == 0 { continue }
// If alias was found, use command alias
if aliases[cmdArgs[0]] != "" {
cmdString = aliases[cmdArgs[0]] + strings.Trim(cmdString, cmdArgs[0])
execCommand(cmdString)
continue
}
// If command is defined in Lua then run it
if commands[cmdArgs[0]] {
err := l.CallByParam(lua.P{
Fn: l.GetField(
l.GetTable(
l.GetGlobal("commanding"),
lua.LString("__commands")),
cmdArgs[0]),
NRet: 0,
Protect: true,
}, luar.New(l, cmdArgs[1:]))
if err != nil {
// TODO: dont panic
panic(err)
}
readline.AddHistory(cmdString)
continue
}
// Last option: use sh interpreter
switch cmdArgs[0] {
case "exit":
os.Exit(0)
default:
err := execCommand(cmdString)
if err != nil {
// If input is incomplete, start multiline prompting
if syntax.IsIncomplete(err) {
sb := &strings.Builder{}
for {
done := StartMultiline(cmdString, sb)
if done {
break
}
}
} else {
if code, ok := interp.IsExitStatus(err); ok {
if code > 0 {
bait.Em.Emit("command.fail", code)
}
}
fmt.Fprintln(os.Stderr, err)
}
} else {
bait.Em.Emit("command.success", nil)
} }
} }
RunInput(input)
} }
} }
func ContinuePrompt(prev string) (string, error) {
fmt.Print(multilinePrompt)
reader := bufio.NewReader(os.Stdin)
// TODO: use readline here?
cont, err := reader.ReadString('\n')
if err != nil {
fmt.Println("")
return "", err
}
cont = strings.TrimSpace(cont)
return prev + "\n" + strings.TrimSuffix(cont, "\n"), nil
}
// This semi cursed function formats our prompt (obviously) // This semi cursed function formats our prompt (obviously)
func fmtPrompt() string { func fmtPrompt() string {
user, _ := user.Current() user, _ := user.Current()
@ -200,100 +156,6 @@ func fmtPrompt() string {
return nprompt return nprompt
} }
func StartMultiline(prev string, sb *strings.Builder) bool {
// sb fromt outside is passed so we can
// save input from previous prompts
if sb.String() == "" { sb.WriteString(prev + "\n") }
fmt.Printf("... ")
reader := bufio.NewReader(os.Stdin)
cont, err := reader.ReadString('\n')
if err == io.EOF {
// Exit when ^D
fmt.Println("")
return true
}
sb.WriteString(cont)
err = execCommand(sb.String())
if err != nil && syntax.IsIncomplete(err) {
return false
}
return true
}
func splitInput(input string) ([]string, string) {
// end my suffering
// TODO: refactor this garbage
quoted := false
startlastcmd := false
lastcmddone := false
cmdArgs := []string{}
sb := &strings.Builder{}
cmdstr := &strings.Builder{}
lastcmd := readline.GetHistory(readline.HistorySize() - 1)
for _, r := range input {
if r == '"' {
// 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 if !quoted && r == '^' && startlastcmd && !lastcmddone {
// if ^ is found, isnt in quotes and is
// the second occurence of the character and is
// the first time "^^" has been used
cmdstr.WriteString(lastcmd)
sb.WriteString(lastcmd)
startlastcmd = !startlastcmd
lastcmddone = !lastcmddone
continue
} else if !quoted && r == '^' && !lastcmddone {
// if ^ is found, isnt in quotes and is the
// first time of starting "^^"
startlastcmd = !startlastcmd
continue
} else {
sb.WriteRune(r)
}
cmdstr.WriteRune(r)
}
if sb.Len() > 0 {
cmdArgs = append(cmdArgs, sb.String())
}
readline.AddHistory(input)
return cmdArgs, cmdstr.String()
}
// Run command in sh interpreter
func execCommand(cmd string) error {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return err
}
runner, _ := interp.New(
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
)
err = runner.Run(context.TODO(), file)
return err
}
// do i even have to say // do i even have to say
func HandleSignals() { func HandleSignals() {
c := make(chan os.Signal) c := make(chan os.Signal)

View File

@ -18,10 +18,50 @@ commander.register('cd', function (args)
if err == 1 then if err == 1 then
print('directory does not exist') print('directory does not exist')
end end
bait.throw('command.fail', nil) bait.throw('command.exit', err)
else bait.throw('command.success', nil) end else bait.throw('command.exit', 0) end
return return
end end
fs.cd(os.getenv 'HOME') fs.cd(os.getenv 'HOME')
bait.throw('command.success', nil) bait.throw('command.exit', 0)
end) end)
commander.register('exit', function()
os.exit(0)
end)
do
local virt_G = { }
setmetatable(_G, {
__index = function (self, key)
local got_virt = virt_G[key]
if got_virt ~= nil then
return got_virt
end
virt_G[key] = os.getenv(key)
return virt_G[key]
end,
__newindex = function (self, 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,
})
bait.catch('command.exit', function ()
for key, value in pairs(virt_G) do
if type(value) == 'string' then
virt_G[key] = os.getenv(key)
end
end
end)
end

184
shell.go 100644
View File

@ -0,0 +1,184 @@
package main
import (
"fmt"
"os"
"bufio"
"context"
"io"
"strings"
"github.com/bobappleyard/readline"
"github.com/yuin/gopher-lua"
"layeh.com/gopher-luar"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
func RunInput(input string) {
// First try to run user input in Lua
err := l.DoString(input)
if err == nil {
// If it succeeds, add to history and prompt again
HandleHistory(input)
bait.Em.Emit("command.exit", 0)
return
}
cmdArgs, cmdString := splitInput(input)
// If alias was found, use command alias
if aliases[cmdArgs[0]] != "" {
cmdString = aliases[cmdArgs[0]] + strings.Trim(cmdString, cmdArgs[0])
}
// If command is defined in Lua then run it
if commands[cmdArgs[0]] {
err := l.CallByParam(lua.P{
Fn: l.GetField(
l.GetTable(
l.GetGlobal("commanding"),
lua.LString("__commands")),
cmdArgs[0]),
NRet: 0,
Protect: true,
}, luar.New(l, cmdArgs[1:]))
if err != nil {
fmt.Fprintln(os.Stderr,
"Error in command:\n\n" + err.Error())
}
if cmdArgs[0] != "exit" { HandleHistory(cmdString) }
return
}
// Last option: use sh interpreter
err = execCommand(cmdString)
if err != nil {
// If input is incomplete, start multiline prompting
if syntax.IsIncomplete(err) {
for {
cmdString, err = ContinuePrompt(strings.TrimSuffix(cmdString, "\\"))
if err != nil { break }
err = execCommand(cmdString)
if syntax.IsIncomplete(err) || strings.HasSuffix(input, "\\") {
continue
} else if code, ok := interp.IsExitStatus(err); ok {
bait.Em.Emit("command.exit", code)
} else if err != nil {
fmt.Fprintln(os.Stderr, err)
bait.Em.Emit("command.exit", 1)
}
break
}
} else {
if code, ok := interp.IsExitStatus(err); ok {
bait.Em.Emit("command.exit", code)
} else { fmt.Fprintln(os.Stderr, err) }
}
} else {
bait.Em.Emit("command.exit", 0)
}
HandleHistory(cmdString)
}
// Run command in sh interpreter
func execCommand(cmd string) error {
file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "")
if err != nil {
return err
}
runner, _ := interp.New(
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
)
err = runner.Run(context.TODO(), file)
return err
}
func splitInput(input string) ([]string, string) {
// end my suffering
// TODO: refactor this garbage
quoted := false
startlastcmd := false
lastcmddone := false
cmdArgs := []string{}
sb := &strings.Builder{}
cmdstr := &strings.Builder{}
lastcmd := readline.GetHistory(readline.HistorySize() - 1)
for _, r := range input {
if r == '"' {
// 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 if !quoted && r == '^' && startlastcmd && !lastcmddone {
// if ^ is found, isnt in quotes and is
// the second occurence of the character and is
// the first time "^^" has been used
cmdstr.WriteString(lastcmd)
sb.WriteString(lastcmd)
startlastcmd = !startlastcmd
lastcmddone = !lastcmddone
continue
} else if !quoted && r == '^' && !lastcmddone {
// if ^ is found, isnt in quotes and is the
// first time of starting "^^"
startlastcmd = !startlastcmd
continue
} else {
sb.WriteRune(r)
}
cmdstr.WriteRune(r)
}
if sb.Len() > 0 {
cmdArgs = append(cmdArgs, sb.String())
}
return cmdArgs, cmdstr.String()
}
func HandleHistory(cmd string) {
readline.AddHistory(cmd)
readline.SaveHistory(homedir + "/.hilbish-history")
// TODO: load history again (history shared between sessions like this ye)
}
func StartMultiline(prev string, sb *strings.Builder) bool {
// sb fromt outside is passed so we can
// save input from previous prompts
if sb.String() == "" { sb.WriteString(prev + "\n") }
fmt.Print(multilinePrompt)
reader := bufio.NewReader(os.Stdin)
cont, err := reader.ReadString('\n')
if err == io.EOF {
// Exit when ^D
fmt.Println("")
return true
}
sb.WriteString(cont)
err = execCommand(sb.String())
if err != nil && syntax.IsIncomplete(err) {
return false
}
return true
}