2021-07-26 05:31:22 +00:00
package main
import (
2021-07-27 05:47:24 +00:00
"flag"
2021-07-31 20:25:20 +00:00
"fmt"
2021-07-27 05:47:24 +00:00
"io/ioutil"
2022-06-08 21:26:30 +00:00
"math/rand"
2021-07-31 20:25:20 +00:00
"os"
2022-05-14 21:13:03 +00:00
"os/exec"
2021-08-02 21:55:59 +00:00
"os/signal"
2021-08-17 05:20:12 +00:00
"path/filepath"
2022-05-14 21:13:03 +00:00
"runtime/debug"
2022-04-05 23:19:58 +00:00
"sort"
2021-07-31 20:25:20 +00:00
"strings"
2021-08-02 21:55:59 +00:00
"syscall"
2022-06-08 21:26:30 +00:00
"time"
2021-07-31 06:31:08 +00:00
ln "github.com/peterh/liner"
2022-12-21 04:32:40 +00:00
"golang.design/x/clipboard"
2021-07-26 05:31:22 +00:00
)
2023-05-09 21:07:19 +00:00
const version = "1.2.14"
2022-04-26 05:36:12 +00:00
2021-12-24 17:55:00 +00:00
const globalLibPath = "/usr/local/lib/slope/modules/"
2021-07-27 16:33:06 +00:00
var openFiles [ ] * IOHandle
2021-07-27 05:47:24 +00:00
var replCounter int = 0
2021-07-31 06:31:08 +00:00
var line * ln . State
2021-08-02 21:37:57 +00:00
var initialTerm ln . ModeApplier = nil
var linerTerm ln . ModeApplier = nil
2021-08-10 03:16:09 +00:00
var panicOnException = true // default to `(exception-mode-panic)`
2021-08-17 05:20:12 +00:00
var ModBaseDir string = ""
2021-08-17 20:44:24 +00:00
var PreloadDir string = ""
2022-05-14 21:13:03 +00:00
var debugMode bool = false
2022-12-21 04:32:40 +00:00
var clipboardErr error
2021-07-26 05:31:22 +00:00
// Used in the repl so that input can continue,
// but potentially useful in other situations
func RecoverError ( ) {
2021-07-31 20:25:20 +00:00
if r := recover ( ) ; r != nil {
2021-08-20 22:18:49 +00:00
if line == nil {
fmt . Fprintf ( os . Stderr , "\033[1mpanic:\033[0m %s\n" , r )
2022-05-14 21:13:03 +00:00
if debugMode {
debug . PrintStack ( )
}
2021-08-20 22:18:49 +00:00
SafeExit ( 1 )
} else {
fmt . Fprintf ( os . Stderr , "\033[31mOops! %s\033[0m\n" , r )
2022-05-14 21:13:03 +00:00
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
2021-08-20 22:18:49 +00:00
}
2021-07-31 20:25:20 +00:00
}
2022-05-14 21:13:03 +00:00
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 ) )
}
2021-07-26 05:31:22 +00:00
}
2021-07-31 17:32:52 +00:00
2021-07-26 05:31:22 +00:00
func Init ( ) {
2021-07-27 16:33:06 +00:00
openFiles = make ( [ ] * IOHandle , 0 , 5 )
2022-12-23 05:45:09 +00:00
addGUIToLib ( ) // Will combine guiLib and stdLib
addClipToLib ( ) // Will combine clipLib and stdLib
addDialogToLib ( ) // Will combine dialogLib and stdLib
2021-07-31 20:25:20 +00:00
globalenv = env { stdLibrary , nil }
2021-08-17 05:20:12 +00:00
ModBaseDir = ExpandedAbsFilepath ( getModBaseDir ( ) )
2021-08-17 20:44:24 +00:00
PreloadDir = ExpandedAbsFilepath ( getPreloadDir ( ) )
createDataDirs ( ModBaseDir )
2021-12-24 17:55:00 +00:00
createDataDirs ( globalLibPath )
2021-08-17 20:44:24 +00:00
createDataDirs ( PreloadDir )
2021-08-02 21:55:59 +00:00
c := make ( chan os . Signal )
signal . Notify ( c , syscall . SIGINT )
go handleSignals ( c )
2022-12-21 04:32:40 +00:00
clipboardErr = clipboard . Init ( )
2022-06-08 21:26:30 +00:00
rand . Seed ( time . Now ( ) . UnixNano ( ) )
2021-07-26 05:31:22 +00:00
}
2022-08-30 22:27:35 +00:00
func outputResult ( txt string , environ * env ) {
2021-07-31 20:25:20 +00:00
defer RecoverError ( )
2021-07-27 05:47:24 +00:00
parsed := Parse ( txt )
for i := range parsed . ( [ ] expression ) {
replCounter ++
2022-08-30 22:27:35 +00:00
evaluated := eval ( parsed . ( [ ] expression ) [ i ] , environ )
2021-08-13 21:11:47 +00:00
if e , ok := evaluated . ( exception ) ; panicOnException && ok {
panic ( string ( e ) )
}
2021-08-12 21:20:02 +00:00
fmt . Printf ( "#%d=> %s\n" , replCounter , String ( evaluated , true ) )
2021-07-26 05:31:22 +00:00
}
2021-07-27 05:47:24 +00:00
}
2021-08-24 21:53:27 +00:00
func Repl ( ) {
initialTerm , _ = ln . TerminalMode ( )
line = ln . NewLiner ( )
2021-09-04 21:14:30 +00:00
line . SetCtrlCAborts ( true )
2021-08-24 21:53:27 +00:00
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 ( )
}
2022-05-11 02:55:06 +00:00
// 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
}
2022-05-25 16:46:23 +00:00
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
2022-05-11 02:55:06 +00:00
2022-04-05 23:05:55 +00:00
// 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 ) ... )
2022-06-17 05:37:03 +00:00
c = append ( c , completeFromMap ( getAllModFuncNames ( ) , l , lastIndex ) ... )
2022-04-05 23:19:58 +00:00
sort . Strings ( c )
2022-04-05 23:05:55 +00:00
return
} )
2021-08-24 21:53:27 +00:00
var text strings . Builder
var cont bool
2022-10-16 22:01:33 +00:00
var raw bool
var match bool
2021-08-24 21:53:27 +00:00
for {
globalenv . vars [ symbol ( "slope-interactive?" ) ] = true
if linerTerm != nil {
linerTerm . ApplyMode ( )
}
2022-08-30 22:27:35 +00:00
in := prompt ( line , cont , "!root!" )
2021-08-24 21:53:27 +00:00
if initialTerm != nil {
initialTerm . ApplyMode ( )
}
2022-10-16 22:01:33 +00:00
if len ( strings . TrimSpace ( in ) ) == 0 && ! raw {
2021-08-24 21:53:27 +00:00
continue
}
text . WriteString ( in )
text . WriteRune ( '\n' )
2022-10-17 02:50:31 +00:00
var brokenString bool
match , raw , brokenString = stringParensMatch ( text . String ( ) )
if ! match && ! brokenString {
2021-08-24 21:53:27 +00:00
cont = true
} else {
cont = false
2022-08-30 22:27:35 +00:00
outputResult ( text . String ( ) , & globalenv )
2021-08-24 21:53:27 +00:00
text . Reset ( )
}
}
}
2021-07-27 05:47:24 +00:00
func RunCommand ( s string ) {
2021-07-31 20:25:20 +00:00
if strings . Count ( s , "(" ) != strings . Count ( s , ")" ) {
2021-07-27 05:47:24 +00:00
fmt . Fprintf ( os . Stderr , "Invalid input string, uneven parens" )
SafeExit ( 1 )
}
2021-08-02 21:37:57 +00:00
globalenv . vars [ symbol ( "slope-interactive?" ) ] = false
2021-07-27 05:47:24 +00:00
p := Parse ( s )
for i := range p . ( [ ] expression ) {
replCounter ++
2021-08-13 21:11:47 +00:00
v := eval ( p . ( [ ] expression ) [ i ] , & globalenv )
if e , ok := v . ( exception ) ; panicOnException && ok {
panic ( string ( e ) )
}
2021-07-27 05:47:24 +00:00
}
SafeExit ( 0 )
}
2021-08-17 21:59:34 +00:00
func RunModule ( path string , relative bool ) ( env , error ) {
2021-12-24 17:55:00 +00:00
modDir := ModBaseDir
2021-08-17 05:20:12 +00:00
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
2021-08-17 21:59:34 +00:00
var s string
if ! relative {
// This branch is run from 'load-mod'
2021-12-24 17:55:00 +00:00
err = os . Chdir ( filepath . Join ( modDir , path ) )
2021-08-17 21:59:34 +00:00
if err != nil {
2021-12-24 17:55:00 +00:00
err = os . Chdir ( filepath . Join ( globalLibPath , path ) )
if err != nil {
return modEnv , err
}
modDir = globalLibPath
2021-08-17 21:59:34 +00:00
}
2021-12-24 17:55:00 +00:00
fp := filepath . Join ( modDir , path , "main.slo" )
2021-08-17 21:59:34 +00:00
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 )
2021-08-17 05:20:12 +00:00
}
p := Parse ( s )
for i := range p . ( [ ] expression ) {
exp , ok := p . ( [ ] expression ) [ i ] . ( [ ] expression )
if ! ok {
continue
}
2021-09-04 14:45:35 +00:00
2021-08-17 05:20:12 +00:00
proc , ok := exp [ 0 ] . ( symbol )
if ! ok {
continue
}
switch proc {
2021-08-17 21:59:34 +00:00
case symbol ( "define" ) , symbol ( "load-mod" ) , symbol ( "load-mod-file" ) :
2021-08-17 05:20:12 +00:00
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 ) )
}
}
}
2021-08-17 21:59:34 +00:00
_ = os . Chdir ( revertToThisDir )
2021-08-17 05:20:12 +00:00
return modEnv , nil
}
2021-08-02 21:37:57 +00:00
func RunFile ( path string , load bool ) error {
2021-09-04 14:45:35 +00:00
replLine := replCounter
2021-08-02 21:37:57 +00:00
globalenv . vars [ symbol ( "slope-interactive?" ) ] = false
2021-07-27 05:47:24 +00:00
b , err := ioutil . ReadFile ( path )
if err != nil {
2021-08-02 21:37:57 +00:00
if load {
return err
}
2021-07-27 05:47:24 +00:00
fmt . Fprintf ( os . Stderr , "Cannot read file %s" , path )
SafeExit ( 1 )
}
s := string ( b )
p := Parse ( s )
for i := range p . ( [ ] expression ) {
replCounter ++
2021-08-13 21:11:47 +00:00
v := eval ( p . ( [ ] expression ) [ i ] , & globalenv )
if e , ok := v . ( exception ) ; panicOnException && ok {
panic ( string ( e ) )
}
2021-07-27 05:47:24 +00:00
}
2021-08-02 21:37:57 +00:00
if load {
2021-08-08 03:03:46 +00:00
replCounter = replLine
2021-08-02 21:37:57 +00:00
return nil
}
2021-07-27 05:47:24 +00:00
SafeExit ( 0 )
2021-08-02 21:37:57 +00:00
return nil // meaningless return since exit will occur
2021-07-26 05:31:22 +00:00
}
2021-07-31 06:31:08 +00:00
2022-08-30 22:27:35 +00:00
func prompt ( l * ln . State , cont bool , inspectionID string ) string {
2021-07-31 06:31:08 +00:00
p := "> "
2022-08-30 22:27:35 +00:00
if inspectionID != "!root!" {
p = inspectionID + "> "
}
2021-07-31 06:31:08 +00:00
if cont {
p = "+ "
}
2021-08-02 21:44:14 +00:00
val , err := l . Prompt ( p )
if err == ln . ErrPromptAborted {
SafeExit ( 1 )
2021-07-31 06:31:08 +00:00
}
2021-08-02 21:44:14 +00:00
l . AppendHistory ( val )
2021-07-31 06:31:08 +00:00
return val
}
2021-07-31 20:25:20 +00:00
func PrintVersion ( ) {
2022-12-23 05:45:09 +00:00
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 , ", " ) )
}
2022-04-05 06:10:04 +00:00
logo := "\033[7m┌─┐┬ ┌─┐┌─┐┌─┐\033[0m\n\033[7m└─┐│ │ │├─┘├┤ \033[0m\n\033[7m└─┘┴─┘└─┘┴ └─┘\033[0m"
2022-12-23 05:45:09 +00:00
fmt . Printf ( "\033[1m%s\033[0m\nVersion: %s\n%s(c) 2021 sloum, see license with: (license)\n" , logo , version , optionalMods )
2021-07-31 20:25:20 +00:00
}
2021-07-26 05:31:22 +00:00
func main ( ) {
2021-08-20 22:18:49 +00:00
defer RecoverError ( )
2021-07-31 20:25:20 +00:00
verFlag := flag . Bool ( "v" , false , "Print version information and exit" )
2021-07-27 05:47:24 +00:00
runCommand := flag . String ( "run" , "" , "A quoted string of commands to run and get output from" )
2021-08-17 20:44:24 +00:00
preloadEnv := flag . Bool ( "L" , false , "Preload all files in the slope preload directory" )
2022-05-14 21:13:03 +00:00
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" )
2021-09-23 02:58:23 +00:00
2021-07-27 05:47:24 +00:00
flag . Parse ( )
args := flag . Args ( )
2021-07-31 20:25:20 +00:00
if * verFlag {
PrintVersion ( )
os . Exit ( 0 )
}
2022-05-14 21:13:03 +00:00
if * install != "" {
BasicInstall ( * install )
}
if * bug {
debugMode = true
}
2021-08-25 21:58:42 +00:00
Init ( )
2021-08-17 20:44:24 +00:00
2021-07-27 05:47:24 +00:00
if * runCommand != "" {
2021-08-17 20:44:24 +00:00
if * preloadEnv {
preloadFiles ( )
}
2022-06-02 16:21:34 +00:00
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 )
}
2021-07-27 05:47:24 +00:00
} else if len ( args ) > 0 {
2021-08-17 20:44:24 +00:00
if * preloadEnv {
preloadFiles ( )
}
2021-08-02 21:37:57 +00:00
RunFile ( args [ 0 ] , false )
2021-07-27 05:47:24 +00:00
} else {
2021-08-17 20:44:24 +00:00
PrintVersion ( )
2022-04-05 06:10:04 +00:00
fmt . Printf ( "Type \033[1m(exit)\033[0m to quit\n\n" )
2021-08-17 20:44:24 +00:00
if * preloadEnv {
preloadFiles ( )
}
2021-07-27 05:47:24 +00:00
Repl ( )
}
2021-07-26 05:31:22 +00:00
}