380 lines
9.3 KiB
Go
380 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
ln "github.com/peterh/liner"
|
|
"golang.design/x/clipboard"
|
|
)
|
|
|
|
const version = "1.2.13"
|
|
|
|
const globalLibPath = "/usr/local/lib/slope/modules/"
|
|
|
|
var openFiles []*IOHandle
|
|
var replCounter int = 0
|
|
var line *ln.State
|
|
var initialTerm ln.ModeApplier = nil
|
|
var linerTerm ln.ModeApplier = nil
|
|
var panicOnException = true // default to `(exception-mode-panic)`
|
|
var ModBaseDir string = ""
|
|
var PreloadDir string = ""
|
|
var debugMode bool = false
|
|
var clipboardErr error
|
|
|
|
// 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)
|
|
if debugMode {
|
|
debug.PrintStack()
|
|
}
|
|
SafeExit(1)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "\033[31mOops! %s\033[0m\n", r)
|
|
if debugMode {
|
|
debug.PrintStack()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func BasicInstall(p string) {
|
|
p = ExpandedAbsFilepath(p)
|
|
ModBaseDir = ExpandedAbsFilepath(getModBaseDir())
|
|
if _, err := os.Stat(filepath.Join(p, "main.slo")); err != nil {
|
|
panic(fmt.Sprintf("Could not install %q, it does not exist or is not a valid module", p))
|
|
}
|
|
newPath := filepath.Join(ModBaseDir, filepath.Base(p))
|
|
if _, err := os.Stat(newPath); err == nil {
|
|
fmt.Printf("The module %q already exists, do you wish to overwrite it? ", filepath.Base(p))
|
|
var answer string
|
|
fmt.Scanln(&answer)
|
|
if !strings.HasPrefix(strings.ToLower(answer), "y") {
|
|
fmt.Println("Install cancelled")
|
|
return
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("cp", "-r", p, newPath)
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Could not install %q, there was an error while copying files", p))
|
|
}
|
|
}
|
|
|
|
func Init() {
|
|
openFiles = make([]*IOHandle, 0, 5)
|
|
addGUIToLib() // Will combine guiLib and stdLib
|
|
addClipToLib() // Will combine clipLib and stdLib
|
|
addDialogToLib() // Will combine dialogLib and stdLib
|
|
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)
|
|
clipboardErr = clipboard.Init()
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
func outputResult(txt string, environ *env) {
|
|
defer RecoverError()
|
|
parsed := Parse(txt)
|
|
for i := range parsed.([]expression) {
|
|
replCounter++
|
|
evaluated := eval(parsed.([]expression)[i], environ)
|
|
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()
|
|
}
|
|
|
|
// Add repl only proc, repl-flush
|
|
globalenv.vars["repl-flush"] = func(a ...expression) expression {
|
|
if f, e := os.Create(histFile); e == nil && line != nil {
|
|
line.WriteHistory(f)
|
|
f.Close()
|
|
}
|
|
return true
|
|
}
|
|
usageStrings["repl-flush"] = "(repl-flush) => #t\n\nForces a write to the repl history file, truncating the file, and writing the contents of the repl sesssion to the file:\n\n\t" + histFile
|
|
|
|
// 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)...)
|
|
c = append(c, completeFromMap(getAllModFuncNames(), l, lastIndex)...)
|
|
sort.Strings(c)
|
|
return
|
|
})
|
|
|
|
var text strings.Builder
|
|
var cont bool
|
|
var raw bool
|
|
var match bool
|
|
for {
|
|
globalenv.vars[symbol("slope-interactive?")] = true
|
|
if linerTerm != nil {
|
|
linerTerm.ApplyMode()
|
|
}
|
|
in := prompt(line, cont, "!root!")
|
|
if initialTerm != nil {
|
|
initialTerm.ApplyMode()
|
|
}
|
|
if len(strings.TrimSpace(in)) == 0 && !raw {
|
|
continue
|
|
}
|
|
text.WriteString(in)
|
|
text.WriteRune('\n')
|
|
var brokenString bool
|
|
match, raw, brokenString = stringParensMatch(text.String())
|
|
if !match && !brokenString {
|
|
cont = true
|
|
} else {
|
|
cont = false
|
|
outputResult(text.String(), &globalenv)
|
|
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, inspectionID string) string {
|
|
p := "> "
|
|
if inspectionID != "!root!" {
|
|
p = inspectionID + "> "
|
|
}
|
|
if cont {
|
|
p = "+ "
|
|
}
|
|
val, err := l.Prompt(p)
|
|
if err == ln.ErrPromptAborted {
|
|
SafeExit(1)
|
|
}
|
|
l.AppendHistory(val)
|
|
return val
|
|
}
|
|
|
|
func PrintVersion() {
|
|
optionalModFormat := "Compiled Modules: %s\n"
|
|
mods := make([]string, 0, 4)
|
|
if clipboardIsOn {
|
|
mods = append(mods, "clipboard")
|
|
}
|
|
if dialogIsOn {
|
|
mods = append(mods, "dialog")
|
|
}
|
|
if guiIsOn {
|
|
mods = append(mods, "gui")
|
|
}
|
|
optionalMods := ""
|
|
if len(mods) > 0 {
|
|
optionalMods = fmt.Sprintf(optionalModFormat, strings.Join(mods, ", "))
|
|
}
|
|
|
|
logo := "\033[7m┌─┐┬ ┌─┐┌─┐┌─┐\033[0m\n\033[7m└─┐│ │ │├─┘├┤ \033[0m\n\033[7m└─┘┴─┘└─┘┴ └─┘\033[0m"
|
|
fmt.Printf("\033[1m%s\033[0m\nVersion: %s\n%s(c) 2021 sloum, see license with: (license)\n", logo, version, optionalMods)
|
|
}
|
|
|
|
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")
|
|
bug := flag.Bool("debug", false, "Turns on debug mode allowing full stack traces of the underlying runtime on panic")
|
|
install := flag.String("install", "", "Install a a module from a local path")
|
|
|
|
flag.Parse()
|
|
args := flag.Args()
|
|
|
|
if *verFlag {
|
|
PrintVersion()
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *install != "" {
|
|
BasicInstall(*install)
|
|
}
|
|
|
|
if *bug {
|
|
debugMode = true
|
|
}
|
|
|
|
Init()
|
|
|
|
if *runCommand != "" {
|
|
if *preloadEnv {
|
|
preloadFiles()
|
|
}
|
|
if *runCommand == "--" {
|
|
b, err := ioutil.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Could not read from stdin")
|
|
SafeExit(1)
|
|
}
|
|
RunCommand(string(b))
|
|
} else {
|
|
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()
|
|
}
|
|
}
|