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() } }