slope/main.go

292 lines
6.9 KiB
Go

package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
ln "github.com/peterh/liner"
)
const globalLibPath = "/usr/local/lib/slope/modules/"
var openFiles []*IOHandle
var replCounter int = 0
var line *ln.State
var VersionHash string
var initialTerm ln.ModeApplier = nil
var linerTerm ln.ModeApplier = nil
var panicOnException = true // default to `(exception-mode-panic)`
var ModBaseDir string = ""
var PreloadDir string = ""
// Used in the repl so that input can continue,
// but potentially useful in other situations
func RecoverError() {
if r := recover(); r != nil {
if line == nil {
fmt.Fprintf(os.Stderr, "\033[1mpanic:\033[0m %s\n", r)
SafeExit(1)
} else {
fmt.Fprintf(os.Stderr, "\033[31mOops! %s\033[0m\n", r)
}
}
}
func Init() {
openFiles = make([]*IOHandle, 0, 5)
addGUIToLib() // Will combine guiLib and stdLib for use in creation of global env
globalenv = env{stdLibrary, nil}
ModBaseDir = ExpandedAbsFilepath(getModBaseDir())
PreloadDir = ExpandedAbsFilepath(getPreloadDir())
createDataDirs(ModBaseDir)
createDataDirs(globalLibPath)
createDataDirs(PreloadDir)
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
go handleSignals(c)
}
func outputResult(txt string) {
defer RecoverError()
parsed := Parse(txt)
for i := range parsed.([]expression) {
replCounter++
evaluated := eval(parsed.([]expression)[i], &globalenv)
if e, ok := evaluated.(exception); panicOnException && ok {
panic(string(e))
}
fmt.Printf("#%d=> %s\n", replCounter, String(evaluated, true))
}
}
func Repl() {
initialTerm, _ = ln.TerminalMode()
line = ln.NewLiner()
line.SetCtrlCAborts(true)
linerTerm, _ = ln.TerminalMode()
defer line.Close()
histFile := ExpandedAbsFilepath(filepath.Join(getModBaseDir(), "..", historyFilename))
if f, e := os.Open(histFile); e == nil {
line.ReadHistory(f)
f.Close()
}
// Set up completion
line.SetTabCompletionStyle(ln.TabCircular)
line.SetCompleter(func(l string) (c []string) {
if len(l) == 0 {
return
}
lastIndex := strings.LastIndexAny(l, "( \n")
c = append(c, completeFromMap(usageStrings, l, lastIndex)...)
for _, v := range moduleUsageStrings {
c = append(c, completeFromMap(v, l, lastIndex)...)
}
sort.Strings(c)
return
})
var text strings.Builder
var cont bool
for {
globalenv.vars[symbol("slope-interactive?")] = true
if linerTerm != nil {
linerTerm.ApplyMode()
}
in := prompt(line, cont)
if initialTerm != nil {
initialTerm.ApplyMode()
}
if len(strings.TrimSpace(in)) == 0 {
continue
}
text.WriteString(in)
text.WriteRune('\n')
if !stringParensMatch(text.String()) {
cont = true
} else {
cont = false
outputResult(text.String())
text.Reset()
}
}
}
func RunCommand(s string) {
if strings.Count(s, "(") != strings.Count(s, ")") {
fmt.Fprintf(os.Stderr, "Invalid input string, uneven parens")
SafeExit(1)
}
globalenv.vars[symbol("slope-interactive?")] = false
p := Parse(s)
for i := range p.([]expression) {
replCounter++
v := eval(p.([]expression)[i], &globalenv)
if e, ok := v.(exception); panicOnException && ok {
panic(string(e))
}
}
SafeExit(0)
}
func RunModule(path string, relative bool) (env, error) {
modDir := ModBaseDir
modEnv := env{make(map[symbol]expression), &globalenv}
revertToThisDir, err := os.Getwd()
if err != nil {
return modEnv, fmt.Errorf("System error during 'load-mod', could not get current working directory")
}
globalenv.vars[symbol("slope-interactive?")] = false
var s string
if !relative {
// This branch is run from 'load-mod'
err = os.Chdir(filepath.Join(modDir, path))
if err != nil {
err = os.Chdir(filepath.Join(globalLibPath, path))
if err != nil {
return modEnv, err
}
modDir = globalLibPath
}
fp := filepath.Join(modDir, path, "main.slo")
b, err := ioutil.ReadFile(fp)
if err != nil {
_ = os.Chdir(revertToThisDir)
return modEnv, fmt.Errorf("Module's main.slo file could not be read")
}
s = string(b)
} else {
// This branch is run from 'load-mod-file'
p, _ := filepath.Abs(path)
if !strings.HasPrefix(p, revertToThisDir) {
return modEnv, fmt.Errorf("An attempt to load a file out of the module directory was made.")
}
b, err := ioutil.ReadFile(p)
if err != nil {
_ = os.Chdir(revertToThisDir)
return modEnv, fmt.Errorf("Could not read module sub-file: %q", path)
}
s = string(b)
}
p := Parse(s)
for i := range p.([]expression) {
exp, ok := p.([]expression)[i].([]expression)
if !ok {
continue
}
proc, ok := exp[0].(symbol)
if !ok {
continue
}
switch proc {
case symbol("define"), symbol("load-mod"), symbol("load-mod-file"):
v := eval(p.([]expression)[i], &modEnv)
if e, ok := v.(exception); panicOnException && ok {
_ = os.Chdir(revertToThisDir)
panic(string(e))
} else if ok {
_ = os.Chdir(revertToThisDir)
return modEnv, fmt.Errorf(string(e))
}
}
}
_ = os.Chdir(revertToThisDir)
return modEnv, nil
}
func RunFile(path string, load bool) error {
replLine := replCounter
globalenv.vars[symbol("slope-interactive?")] = false
b, err := ioutil.ReadFile(path)
if err != nil {
if load {
return err
}
fmt.Fprintf(os.Stderr, "Cannot read file %s", path)
SafeExit(1)
}
s := string(b)
p := Parse(s)
for i := range p.([]expression) {
replCounter++
v := eval(p.([]expression)[i], &globalenv)
if e, ok := v.(exception); panicOnException && ok {
panic(string(e))
}
}
if load {
replCounter = replLine
return nil
}
SafeExit(0)
return nil // meaningless return since exit will occur
}
func prompt(l *ln.State, cont bool) string {
p := "> "
if cont {
p = "+ "
}
val, err := l.Prompt(p)
if err == ln.ErrPromptAborted {
SafeExit(1)
}
l.AppendHistory(val)
return val
}
func PrintVersion() {
logo := "\033[7m┌─┐┬ ┌─┐┌─┐┌─┐\033[0m\n\033[7m└─┐│ │ │├─┘├┤ \033[0m\n\033[7m└─┘┴─┘└─┘┴ └─┘\033[0m"
if VersionHash != "" {
fmt.Printf("\033[1m%s\033[0m\nBuild Version Hash/Commit: %s %s\n(c) 2021 sloum, see license with: (license)\n", logo, VersionHash, guiIsOn)
} else {
fmt.Printf("\033[1m%s\033[0m\nBuild Version Hash/Commit: Custom Build (not hash based)\n(c) 2021 sloum, see license with: \033[1m(license)\033[0m\n", logo)
}
}
func main() {
defer RecoverError()
verFlag := flag.Bool("v", false, "Print version information and exit")
runCommand := flag.String("run", "", "A quoted string of commands to run and get output from")
preloadEnv := flag.Bool("L", false, "Preload all files in the slope preload directory")
flag.Parse()
args := flag.Args()
if *verFlag {
PrintVersion()
os.Exit(0)
}
Init()
if *runCommand != "" {
if *preloadEnv {
preloadFiles()
}
RunCommand(*runCommand)
} else if len(args) > 0 {
if *preloadEnv {
preloadFiles()
}
RunFile(args[0], false)
} else {
PrintVersion()
fmt.Printf("Type \033[1m(exit)\033[0m to quit\n\n")
if *preloadEnv {
preloadFiles()
}
Repl()
}
}