slope/main.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.14"
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()
}
}