slope/helpers.go

592 lines
12 KiB
Go

package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
"unicode"
"git.rawtext.club/sloum/slope/termios"
)
func AnythingToBool(e expression) expression {
switch i := e.(type) {
case bool:
return i
default:
return true
}
}
func MergeSort(slice []expression, sublistIndex int) []expression {
if len(slice) < 2 {
return slice
}
mid := (len(slice)) / 2
return Merge(MergeSort(slice[:mid], sublistIndex), MergeSort(slice[mid:], sublistIndex), sublistIndex)
}
func Merge(left, right []expression, ind int) []expression {
size, i, j := len(left)+len(right), 0, 0
slice := make([]expression, size, size)
if ind >= 0 {
for k := 0; k < size; k++ {
if i > len(left)-1 && j <= len(right)-1 {
slice[k] = right[j]
j++
continue
} else if j > len(right)-1 && i <= len(left)-1 {
slice[k] = left[i]
i++
continue
}
i1, i1IsNumber := left[i].([]expression)[ind].(number)
i2, i2IsNumber := right[j].([]expression)[ind].(number)
if (i1IsNumber && i2IsNumber && i1 < i2) || (i1IsNumber && !i2IsNumber) {
slice[k] = left[i]
i++
} else if !i1IsNumber && i2IsNumber {
slice[k] = right[j]
j++
} else if (!i1IsNumber && !i2IsNumber) && String(left[i].([]expression)[ind], false) < String(right[j].([]expression)[ind], false) {
slice[k] = left[i]
i++
} else {
slice[k] = right[j]
j++
}
}
} else {
for k := 0; k < size; k++ {
if i > len(left)-1 && j <= len(right)-1 {
slice[k] = right[j]
j++
continue
} else if j > len(right)-1 && i <= len(left)-1 {
slice[k] = left[i]
i++
continue
}
i1, i1IsNumber := left[i].(number)
i2, i2IsNumber := right[j].(number)
if (i1IsNumber && i2IsNumber && float64(i1) < float64(i2)) || (!i1IsNumber && i2IsNumber) {
slice[k] = left[i]
i++
} else if i1IsNumber && !i2IsNumber {
slice[k] = right[j]
j++
} else if (!i1IsNumber && !i2IsNumber) && String(left[i], false) < String(right[j], false) {
slice[k] = left[i]
i++
} else {
slice[k] = right[j]
j++
}
}
}
return slice
}
func MergeLists(a ...expression) (expression, error) {
length := -1
for i := range a[0].([]expression) {
if _, ok := a[0].([]expression)[i].([]expression); !ok {
return 0, fmt.Errorf("a value other than a list was provided")
}
if length == -1 {
length = len(a[0].([]expression)[i].([]expression))
} else if length != len(a[0].([]expression)[i].([]expression)) {
return 0, fmt.Errorf("lists are of unequal length")
}
}
mergedLists := make([]expression, 0, len(a[0].([]expression)[0].([]expression)))
for i, _ := range a[0].([]expression)[0].([]expression) {
lineList := make([]expression, 0, len(a[0].([]expression)))
for l, _ := range a[0].([]expression) {
lineList = append(lineList, a[0].([]expression)[l].([]expression)[i])
}
mergedLists = append(mergedLists, lineList)
}
return mergedLists, nil
}
func escapeString(s string) string {
var out strings.Builder
for _, c := range []rune(s) {
switch c {
case '\t':
out.WriteString("\\t")
case '\n':
out.WriteString("\\n")
case '\r':
out.WriteString("\\r")
case '\v':
out.WriteString("\\v")
case '\a':
out.WriteString("\\a")
case '\b':
out.WriteString("\\b")
case '\f':
out.WriteString("\\f")
case '\\':
out.WriteString("\\\\")
default:
if !unicode.IsPrint(c) {
out.WriteString(fmt.Sprintf("\\0%o", c))
} else {
out.WriteRune(c)
}
}
}
return out.String()
}
func unescapeString(s string) string {
var out strings.Builder
escapeNumBase := 10
var altNum bool
var otherNum strings.Builder
var slash bool
for _, c := range []rune(s) {
if slash && !altNum {
switch c {
case 't':
out.WriteRune('\t')
case 'n':
out.WriteRune('\n')
case 'r':
out.WriteRune('\r')
case 'v':
out.WriteRune('\v')
case 'a':
out.WriteRune('\a')
case 'b':
out.WriteRune('\b')
case '\\':
out.WriteRune('\\')
case 'f':
out.WriteRune('\f')
case '0':
escapeNumBase = 8
altNum = true
continue
case '1', '3', '4', '2', '5', '6', '7', '8', '9':
altNum = true
otherNum.WriteRune(c)
continue
default:
out.WriteRune(c)
}
slash = false
} else if slash {
switch c {
case '0', '1', '3', '4', '2', '5', '6', '7', '8', '9':
otherNum.WriteRune(c)
case 'x':
if otherNum.String() == "" {
escapeNumBase = 16
continue
}
fallthrough
default:
altNum = false
slash = false
if otherNum.Len() > 0 {
i, err := strconv.ParseInt(otherNum.String(), escapeNumBase, 64)
if err == nil {
out.WriteRune(rune(i))
} else {
out.WriteRune('?')
}
otherNum.Reset()
}
if c == '\\' {
slash = true
} else {
out.WriteRune(c)
}
}
} else if c == '\\' {
slash = true
} else {
out.WriteRune(c)
}
}
if otherNum.Len() > 0 {
i, err := strconv.ParseInt(otherNum.String(), escapeNumBase, 64)
if err == nil {
out.WriteRune(rune(i))
} else {
out.WriteRune('?')
}
}
return out.String()
}
func SafeExit(code int) {
for i := range openFiles {
if !openFiles[i].Open {
continue
}
switch o := openFiles[i].Obj.(type) {
case *os.File:
o.Close()
openFiles[i].Open = false
case *net.Conn:
(*o).Close()
openFiles[i].Open = false
case *tls.Conn:
o.Close()
openFiles[i].Open = false
}
}
histFile := ExpandedAbsFilepath(filepath.Join(getModBaseDir(), "..", historyFilename))
if f, e := os.Create(histFile); e == nil && line != nil {
line.WriteHistory(f)
f.Close()
}
if linerTerm != nil || initialTerm != nil {
line.Close()
}
termios.Restore()
os.Exit(code)
}
func StringSliceToExpressionSlice(s []string) expression {
e := make([]expression, len(s))
for i := range s {
e[i] = s[i]
}
return e
}
func loadFiles(files []expression) {
for _, v := range files {
err := RunFile(ExpandedAbsFilepath(v.(string)), true)
if err != nil {
panic(err.Error())
}
}
}
type Module struct {
text string
notes []string
dependencies []string
description string
source string
entry string
}
func ParseModFile(p string) (Module, error) {
b, err := ioutil.ReadFile(p)
if err != nil {
return Module{}, fmt.Errorf("Could not load modfile: %s", p)
}
s := string(b)
lines := strings.Split(s, "\n")
var mod Module
mod.text = s
for _, line := range lines {
fields := strings.SplitN(line, " ", 2)
if len(fields) != 2 {
continue
}
fields[1] = strings.TrimSpace(fields[1])
switch strings.ToLower(fields[0]) {
case "entry":
mod.entry = fields[1]
case "source":
mod.source = fields[1]
default:
continue
}
}
if mod.entry == "" && mod.source == "" {
return mod, fmt.Errorf("Modfile missing entry and source fields")
} else if mod.entry == "" {
return mod, fmt.Errorf("Modfile missing entry field")
} else if mod.source == "" {
return mod, fmt.Errorf("Modfile missing source field")
}
return mod, nil
}
func ExpandedAbsFilepath(p string) string {
if strings.HasPrefix(p, "~") {
if p == "~" || strings.HasPrefix(p, "~/") {
homedir, _ := os.UserHomeDir()
if len(p) <= 2 {
p = homedir
} else if len(p) > 2 {
p = filepath.Join(homedir, p[2:])
}
} else {
i := strings.IndexRune(p, '/')
var u string
var remainder string
if i < 0 {
u = p[1:]
remainder = ""
} else {
u = p[1:i]
remainder = p[i:]
}
usr, err := user.Lookup(u)
if err != nil {
p = filepath.Join("/home", u, remainder)
} else {
p = filepath.Join(usr.HomeDir, remainder)
}
}
} else if !strings.HasPrefix(p, "/") {
wd, _ := os.Getwd()
p = filepath.Join(wd, p)
}
path, _ := filepath.Abs(p)
return path
}
func handleSignals(c <-chan os.Signal) {
for {
switch <-c {
case syscall.SIGINT:
apply(globalenv.vars["__SIGINT"], make([]expression, 0))
}
}
}
func formatValue(format string, value expression) (string, error) {
v := String(value, false)
left := false
count := 0
if len(format) > 0 {
i, err := strconv.Atoi(format)
if err != nil {
return v, err
}
if i < 0 {
left = true
i = i * -1
}
count = i
}
if count <= len([]rune(v)) {
return v, nil
}
if left {
return v + strings.Repeat(" ", count-len(v)), nil
}
return strings.Repeat(" ", count-len(v)) + v, nil
}
func getModBaseDir() string {
p := os.Getenv("SLOPE_MOD_PATH")
if p == "" {
x := os.Getenv("XDG_DATA_HOME")
if x == "" {
return ExpandedAbsFilepath("~/.local/share/slope/modules/")
}
return filepath.Join(x, "slope", "modules")
}
return p
}
func createDataDirs(p string) {
p = ExpandedAbsFilepath(p)
_, err := os.Stat(p)
if os.IsNotExist(err) {
os.MkdirAll(p, 0755)
}
}
func getPreloadDir() string {
p := os.Getenv("SLOPE_PRELOAD_DIR")
if p == "" {
x := os.Getenv("XDG_DATA_HOME")
if x == "" {
return ExpandedAbsFilepath("~/.local/share/slope/preload/")
}
return filepath.Join(x, "slope", "preload")
}
return p
}
func preloadFiles() {
files, err := filepath.Glob(filepath.Join(PreloadDir, "*.slo"))
if err != nil {
fmt.Fprint(os.Stderr, "Could not preload files. Reading preload directory failed\n")
}
exp := make([]expression, len(files))
for i := range files {
f, err := os.Stat(files[i])
if err != nil || f.IsDir() {
continue
}
exp[i] = filepath.Join(files[i])
}
loadFiles(exp)
}
func percentToDate(c rune) string {
switch c {
case 'a':
return "pm" // 12 hour segment lowercase
case 'A':
return "PM" // 12 hour segment uppercase
case 'd':
return "2" // Day, no leading zero
case 'D':
return "02" // Day, leading zero
case 'e':
return "_2" // Day, no leading zero, with space padding
case 'f':
return "Jan" // Full month, short
case 'F':
return "January" // Full month, long
case 'g', 'G':
return "15" // Hour, 24 hour format
case 'h':
return "3" // Hour, 12 hour format, no leading zero
case 'H':
return "03" // Hour, 12 hour format w/ leading zero
case 'i':
return "4" // Minutes, no leading zero
case 'I':
return "04" // Minutes, leading zero
case 'm':
return "1" // Month number, no leading zero
case 'M':
return "01" // Month number, leading zero
case 'o':
return "-07" // Timezone offset, only hours
case 'O':
return "-0700" // Timezone offset, hours and minutes
case 's':
return "5" // Seconds, no leading zero
case 'S':
return "05" // Seconds, leading zero
case 'w':
return "Mon" // Weekday, short
case 'W':
return "Monday" // Weekday, long
case 'y':
return "06" // Year, two digit
case 'Y':
return "2006" // Year, four digit
case 'Z':
return "MST" // Time zone as three chars
case '%':
return "%" // Literal percent
default:
return "?" // Unknown escape sequence
}
}
func createTimeFormatString(s string) string {
var out strings.Builder
r := strings.NewReader(s)
for {
c, count, err := r.ReadRune()
if err != nil || count == 0 {
break
}
switch c {
case '%':
c, count, err = r.ReadRune()
if err != nil || count == 0 {
out.WriteString(percentToDate('%'))
break
}
out.WriteString(percentToDate(c))
default:
out.WriteRune(c)
}
}
return out.String()
}
// Used by REPL to know if another input line
// should be offered before parsing
func stringParensMatch(s string) bool {
count := 0
inString := false
prevPrev := rune(0)
prev := ' '
for _, c := range s {
switch c {
case '(':
if !inString && prev != '\'' {
count++
}
case ')':
if !inString {
count--
}
case '"':
if !inString {
inString = true
} else if prev != '\\' || (prev == '\\' && prevPrev == '\\') {
inString = false
}
}
prevPrev = c
prev = c
}
if count > 0 {
return false
}
// If count is negative still return true, the
// parser will handle erroring
return true
}
func variadic(l expression) int {
list, ok := l.([]expression)
if !ok || len(list) == 0 {
return 0
}
v, ok := list[len(list)-1].(symbol)
if !ok {
return 0
}
if v == symbol("args-list") {
return 1
}
return 0
}
func completeFromMap(m map[string]string, input string, index int) (c []string) {
for k, _ := range m {
if index < 0 && strings.HasPrefix(string(k), input) {
c = append(c, string(k))
} else if len(input) > index+1 && strings.HasPrefix(string(k), input[index+1:]) {
start := len(input) - index - 1
if start < 0 {
start = 0
}
c = append(c, input+string(k)[start:])
}
}
return
}