689 lines
20 KiB
Go
689 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/chzyer/readline"
|
|
)
|
|
|
|
var assignmentRegex = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$`)
|
|
var funcAssignmentRegex = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)\s*=\s*(.*)$`)
|
|
|
|
func isReservedName(name string) bool {
|
|
upper := strings.ToUpper(name)
|
|
if upper == "PI" || upper == "E" { return true }
|
|
lower := strings.ToLower(name)
|
|
switch lower {
|
|
case "help", "units", "rates", "base", "clear", "exit", "quit", "var", "cur", "unset", "funcs", "ans", "_":
|
|
return true
|
|
}
|
|
switch lower {
|
|
case "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh",
|
|
"sqrt", "log", "log2", "ln", "exp", "abs", "ceil", "floor", "round", "pow", "min", "max", "mod", "fact", "if",
|
|
"ip", "cidr", "network", "broadcast", "mask", "hosts", "range":
|
|
return true
|
|
}
|
|
if _, ok := unitRegistry[upper]; ok { return true }
|
|
return false
|
|
}
|
|
|
|
func loadVariables(path string) error {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("could not open variables file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
decoder := json.NewDecoder(file)
|
|
if err := decoder.Decode(&variables); err != nil {
|
|
return fmt.Errorf("could not decode variables json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func saveVariables(path string) error {
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create variables file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
encoder := json.NewEncoder(file)
|
|
encoder.SetIndent("", " ")
|
|
if err := encoder.Encode(variables); err != nil {
|
|
return fmt.Errorf("could not encode variables json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadUserFuncs(path string) error {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("could not open functions file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
decoder := json.NewDecoder(file)
|
|
if err := decoder.Decode(&userFuncs); err != nil {
|
|
return fmt.Errorf("could not decode functions json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func saveUserFuncs(path string) error {
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create functions file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
encoder := json.NewEncoder(file)
|
|
encoder.SetIndent("", " ")
|
|
if err := encoder.Encode(userFuncs); err != nil {
|
|
return fmt.Errorf("could not encode functions json: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
|
|
func printHelp() {
|
|
fmt.Println("\033[1;34mgoca Help\033[0m")
|
|
fmt.Println("\033[1mArithmetic & Operators:\033[0m")
|
|
fmt.Println(" Basic: +, -, *, /, % (modulo)")
|
|
fmt.Println(" Power: a ** b or pow(a, b)")
|
|
fmt.Println(" Bitwise: & (AND), | (OR), ^ (XOR), ~ (NOT), << (Left Shift), >> (Right Shift)")
|
|
fmt.Println(" Logic: ==, !=, <, >, <=, >=, ! (NOT)")
|
|
fmt.Println(" Implicit: 2(3 + 4) or 2m (implicit multiplication)")
|
|
fmt.Println(" Numbers: Decimal (10), Hex (0xFF), Binary (0b1010), Octal (0o77)")
|
|
fmt.Println(" Variables: Assign (e.g. x = 5), Use 'ans' or '_' for last result")
|
|
fmt.Println(" Custom Fn: f(x, y) = x * y + 2")
|
|
fmt.Println("\033[1mScientific Functions:\033[0m")
|
|
fmt.Println(" Trig: sin(x), cos(x), tan(x) (accepts angles like 90 deg or pi rad)")
|
|
fmt.Println(" Inv Trig: asin(x), acos(x), atan(x) (returns angles, e.g., asin(1) to deg)")
|
|
fmt.Println(" Hyper: sinh(x), cosh(x), tanh(x)")
|
|
fmt.Println(" General: sqrt(x), abs(x), exp(x), ln(x) (natural), log(x) (base 10), log2(x)")
|
|
fmt.Println(" Rounding: ceil(x), floor(x), round(x)")
|
|
fmt.Println(" Stats: min(a, b, ...), max(a, b, ...), mod(a, b), fact(x) (factorial)")
|
|
fmt.Println(" Logic: if(cond, true_val, false_val)")
|
|
fmt.Println(" Constants: PI, E")
|
|
fmt.Println("\033[1mIP & Subnet:\033[0m")
|
|
fmt.Println(" Parse: ip(\"192.168.1.1\"), cidr(\"10.0.0.0/24\")")
|
|
fmt.Println(" Subnet: network(c), broadcast(c), mask(c), hosts(c), range(c)")
|
|
fmt.Println("\033[1mUnits & Conversions:\033[0m")
|
|
fmt.Println(" Syntax: <value> <unit> to/in <unit> (e.g., 10 mi to km, 1 GB in MB)")
|
|
fmt.Println(" Types: Length, Mass, Time, Digital, Area, Temperature, Angle")
|
|
fmt.Println(" Example: 0 C to K, 90 deg to rad, 32 F to C")
|
|
fmt.Println("\033[1mCurrencies (Live Rates):\033[0m")
|
|
fmt.Println(" Usage: Exchange currency codes (e.g., 100 USD to EUR)")
|
|
fmt.Println(" Symbols: $, €, £, ¥ (e.g., 10$ to €)")
|
|
fmt.Println("\033[1mCommands:\033[0m")
|
|
fmt.Println(" help Show this help information")
|
|
fmt.Println(" units List all supported measurement units")
|
|
fmt.Println(" rates Show common currency exchange rates relative to base")
|
|
fmt.Println(" cur List all supported live currency codes")
|
|
fmt.Println(" var List all user-defined variables")
|
|
fmt.Println(" funcs List all user-defined functions")
|
|
fmt.Println(" unset <v> Delete variable <v> (or 'unset *' to delete all)")
|
|
fmt.Println(" base <C> Change base currency (e.g., base USD)")
|
|
fmt.Println(" clear Clear the terminal screen")
|
|
fmt.Println(" exit/quit Exit goca")
|
|
fmt.Println("\033[1mUpdates:\033[0m")
|
|
fmt.Println(" goca --version Print the version")
|
|
fmt.Println(" goca --check-update Look for a newer release")
|
|
fmt.Println(" goca --update Download and install the newest release")
|
|
fmt.Println(" goca looks for a new release once a day, in the background, and says so on")
|
|
fmt.Println(" stderr. GOCA_NO_UPDATE_CHECK=1 or -y turns that off.")
|
|
}
|
|
|
|
type CalcCompleter struct{}
|
|
|
|
func (c *CalcCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
|
|
var wordStart = pos
|
|
for wordStart > 0 {
|
|
ch := line[wordStart-1]
|
|
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' {
|
|
wordStart--
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
word := string(line[wordStart:pos])
|
|
if word == "" {
|
|
return nil, 0
|
|
}
|
|
|
|
allCandidates := []string{
|
|
"help", "units", "rates", "cur", "var", "funcs", "clear", "exit", "quit", "base", "unset",
|
|
"sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh",
|
|
"sqrt", "log", "log2", "ln", "exp", "abs", "ceil", "floor", "round", "pow", "min", "max", "mod", "fact", "if",
|
|
"ip", "cidr", "network", "broadcast", "mask", "hosts", "range",
|
|
"PI", "E", "ans", "_",
|
|
}
|
|
for u := range unitRegistry {
|
|
allCandidates = append(allCandidates, strings.ToLower(u))
|
|
allCandidates = append(allCandidates, u)
|
|
}
|
|
|
|
var matches []string
|
|
seen := make(map[string]bool)
|
|
for _, cand := range allCandidates {
|
|
if strings.HasPrefix(strings.ToLower(cand), strings.ToLower(word)) {
|
|
if !seen[cand] {
|
|
seen[cand] = true
|
|
matches = append(matches, cand)
|
|
}
|
|
}
|
|
}
|
|
for v := range variables {
|
|
if strings.HasPrefix(strings.ToLower(v), strings.ToLower(word)) {
|
|
if !seen[v] {
|
|
seen[v] = true
|
|
matches = append(matches, v)
|
|
}
|
|
for uf := range userFuncs {
|
|
if strings.HasPrefix(strings.ToLower(uf), strings.ToLower(word)) {
|
|
if !seen[uf] {
|
|
seen[uf] = true
|
|
matches = append(matches, uf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Strings(matches)
|
|
|
|
var ret [][]rune
|
|
for _, m := range matches {
|
|
suffix := m[len(word):]
|
|
isFunc := false
|
|
switch m {
|
|
case "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh",
|
|
"sqrt", "log", "log2", "ln", "exp", "abs", "ceil", "floor", "round", "pow", "min", "max", "mod", "fact", "if",
|
|
"ip", "cidr", "network", "broadcast", "mask", "hosts", "range":
|
|
isFunc = true
|
|
}
|
|
if isFunc || func() bool { _, ok := userFuncs[m]; return ok }() {
|
|
suffix += "("
|
|
}
|
|
ret = append(ret, []rune(suffix))
|
|
}
|
|
|
|
return ret, len(word)
|
|
}
|
|
|
|
func printResult(res Result, target string) {
|
|
if res.Dim == DimString {
|
|
fmt.Printf("\033[1;33m= \"%s\"\033[0m\n", stringTable[res.Value.IntPart()])
|
|
return
|
|
}
|
|
|
|
val := res.Value
|
|
if target != "" {
|
|
fmt.Printf("\033[1;33m= %v %s\033[0m\n", val, target)
|
|
} else if res.Dim == DimCurrency {
|
|
rateToBase := d(1.0).Div(eurRatesLookup(baseCurrency))
|
|
result := val.Mul(rateToBase)
|
|
|
|
if result.IsInteger() {
|
|
i := result.IntPart()
|
|
fmt.Printf("\033[1;33m%d %s\033[0m", i, baseCurrency)
|
|
if baseCurrency == "EUR" || baseCurrency == "USD" {
|
|
fmt.Printf(" (Hex: 0x%X, Bin: 0b%b)", i, i)
|
|
}
|
|
fmt.Println()
|
|
} else {
|
|
f, _ := result.Float64()
|
|
fmt.Printf("\033[1;33m= %.4f %s\033[0m\n", f, baseCurrency)
|
|
}
|
|
} else if res.Dim == DimIP {
|
|
i := val.IntPart()
|
|
fmt.Printf("\033[1;33m= %d.%d.%d.%d\033[0m\n", (i>>24)&0xFF, (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF)
|
|
} else if res.Dim == DimCIDR {
|
|
i := val.IntPart()
|
|
ipPart := i >> 8
|
|
mask := i & 0xFF
|
|
fmt.Printf("\033[1;33m= %d.%d.%d.%d/%d\033[0m\n", (ipPart>>24)&0xFF, (ipPart>>16)&0xFF, (ipPart>>8)&0xFF, ipPart&0xFF, mask)
|
|
} else if res.Dim != DimNone {
|
|
label := ""
|
|
for _, v := range unitRegistry {
|
|
if v.Dimension == res.Dim && v.Factor.Equal(d(1.0)) {
|
|
label = v.Label
|
|
break
|
|
}
|
|
}
|
|
fmt.Printf("\033[1;33m= %v %s\033[0m\n", val, label)
|
|
} else {
|
|
if val.IsInteger() {
|
|
i := val.IntPart()
|
|
fmt.Printf("\033[1;33m%d\033[0m (0x%X, 0b%b)\n", i, i, i)
|
|
} else {
|
|
fmt.Printf("\033[1;33m= %v\033[0m\n", val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleLine(line string, useReadline bool, variablesFile string, functionsFile string) bool {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
return false
|
|
}
|
|
|
|
lower := strings.ToLower(line)
|
|
if lower == "exit" || lower == "quit" {
|
|
return true
|
|
}
|
|
if lower == "clear" {
|
|
if useReadline {
|
|
fmt.Print("\033[2J\033[H")
|
|
}
|
|
return false
|
|
}
|
|
if lower == "help" {
|
|
printHelp()
|
|
return false
|
|
}
|
|
if lower == "units" {
|
|
fmt.Println("Supported Units:")
|
|
fmt.Println(" Length: km, m, dm, cm, mm, um, nm, mi, nmi, ft, in, yd")
|
|
fmt.Println(" Mass: t, kg, g, mg, ct, lb, oz")
|
|
fmt.Println(" Time: yr, wk, d, h, min, s, ms, us, ns")
|
|
fmt.Println(" Digital: EB, PB, TB, GB, MB, KB, B")
|
|
fmt.Println(" Area: km2, ha, acre, m2, cm2, mm2")
|
|
fmt.Println(" Temperature: K, C, F")
|
|
fmt.Println(" Angle: rad, deg, grad")
|
|
return false
|
|
}
|
|
if lower == "rates" {
|
|
fmt.Printf("Exchange rates (relative to %s):\n", baseCurrency)
|
|
rateToBase := d(1.0).Div(eurRatesLookup(baseCurrency))
|
|
for _, c := range []string{"EUR", "USD", "GBP", "JPY", "CHF", "CNY"} {
|
|
if r, ok := unitRegistry[c]; ok {
|
|
f, _ := r.Factor.Mul(rateToBase).Float64()
|
|
fmt.Printf(" 1 %s = %.4f %s\n", c, f, baseCurrency)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if lower == "cur" {
|
|
var currencies []string
|
|
for code, u := range unitRegistry {
|
|
if u.Dimension == DimCurrency {
|
|
currencies = append(currencies, code)
|
|
}
|
|
}
|
|
sort.Strings(currencies)
|
|
fmt.Println("Supported Currencies:")
|
|
fmt.Println(" " + strings.Join(currencies, ", "))
|
|
return false
|
|
}
|
|
if lower == "funcs" {
|
|
if len(userFuncs) == 0 {
|
|
fmt.Println("No user-defined functions.")
|
|
return false
|
|
}
|
|
fmt.Println("Defined Functions:")
|
|
var keys []string
|
|
for k := range userFuncs {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, name := range keys {
|
|
uf := userFuncs[name]
|
|
fmt.Printf(" %s(%s) = %s\n", name, strings.Join(uf.Args, ", "), uf.Expr)
|
|
}
|
|
return false
|
|
}
|
|
|
|
if lower == "var" {
|
|
if len(variables) == 0 {
|
|
fmt.Println("No variables defined.")
|
|
return false
|
|
}
|
|
fmt.Println("Defined Variables:")
|
|
var keys []string
|
|
for k := range variables {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, name := range keys {
|
|
res := variables[name]
|
|
if res.Dim == DimCurrency {
|
|
fmt.Printf(" %s = %v (Currency)\n", name, res.Value)
|
|
} else if res.Dim != DimNone {
|
|
label := ""
|
|
for _, v := range unitRegistry {
|
|
if v.Dimension == res.Dim && v.Factor.Equal(d(1.0)) {
|
|
label = v.Label
|
|
break
|
|
}
|
|
}
|
|
fmt.Printf(" %s = %v %s\n", name, res.Value, label)
|
|
} else {
|
|
fmt.Printf(" %s = %v\n", name, res.Value)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if strings.HasPrefix(lower, "base ") {
|
|
newBase := strings.TrimSpace(line[5:])
|
|
switch newBase {
|
|
case "$":
|
|
newBase = "USD"
|
|
case "€":
|
|
newBase = "EUR"
|
|
case "£":
|
|
newBase = "GBP"
|
|
case "¥":
|
|
newBase = "JPY"
|
|
default:
|
|
newBase = strings.ToUpper(newBase)
|
|
}
|
|
if _, ok := unitRegistry[newBase]; ok && unitRegistry[newBase].Dimension == DimCurrency {
|
|
baseCurrency = newBase
|
|
fmt.Printf("Base set to %s\n", baseCurrency)
|
|
} else {
|
|
fmt.Printf("Error: Unknown currency %s\n", newBase)
|
|
}
|
|
return false
|
|
}
|
|
if strings.HasPrefix(lower, "unset ") {
|
|
target := strings.TrimSpace(line[6:])
|
|
if target == "*" || strings.ToLower(target) == "all" {
|
|
if len(variables) == 0 && len(userFuncs) == 0 {
|
|
fmt.Println("No variables or functions defined.")
|
|
} else {
|
|
variables = make(map[string]Result)
|
|
if err := saveVariables(variablesFile); err != nil {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to clear persisted variables: %v\n", err)
|
|
}
|
|
userFuncs = make(map[string]UserFunc)
|
|
if err := saveUserFuncs(functionsFile); err != nil {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to clear persisted functions: %v\n", err)
|
|
}
|
|
fmt.Println("All variables and functions deleted.")
|
|
}
|
|
} else {
|
|
found := false
|
|
if _, ok := variables[target]; ok {
|
|
delete(variables, target)
|
|
if err := saveVariables(variablesFile); err != nil {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist variables: %v\n", err)
|
|
}
|
|
fmt.Printf("Variable '%s' deleted.\n", target)
|
|
found = true
|
|
}
|
|
if _, ok := userFuncs[target]; ok {
|
|
delete(userFuncs, target)
|
|
if err := saveUserFuncs(functionsFile); err != nil {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist functions: %v\n", err)
|
|
}
|
|
fmt.Printf("Function '%s' deleted.\n", target)
|
|
found = true
|
|
}
|
|
if !found {
|
|
fmt.Printf("\033[1;31mError:\033[0m Variable or function '%s' not found.\n", target)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if matches := funcAssignmentRegex.FindStringSubmatch(line); matches != nil {
|
|
name := matches[1]
|
|
argsStr := matches[2]
|
|
exprStr := matches[3]
|
|
|
|
if isReservedName(name) {
|
|
fmt.Printf("\033[1;31mError:\033[0m %s is a reserved name (constant, unit, function, or command)\n", name)
|
|
return false
|
|
}
|
|
|
|
var args []string
|
|
for _, a := range strings.Split(argsStr, ",") {
|
|
a = strings.TrimSpace(a)
|
|
if a != "" {
|
|
args = append(args, a)
|
|
}
|
|
}
|
|
|
|
p := NewParser(exprStr)
|
|
_, err := p.Parse()
|
|
if err != nil {
|
|
fmt.Printf("\033[1;31mError:\033[0m Failed to parse function body: %v\n", err)
|
|
return false
|
|
}
|
|
|
|
userFuncs[name] = UserFunc{Args: args, Expr: exprStr}
|
|
if err := saveUserFuncs(functionsFile); err != nil {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist functions: %v\n", err)
|
|
}
|
|
fmt.Printf("\033[1;33mDefined function %s(%s)\033[0m\n", name, strings.Join(args, ", "))
|
|
return false
|
|
}
|
|
|
|
if matches := assignmentRegex.FindStringSubmatch(line); matches != nil {
|
|
name := matches[1]
|
|
expr := matches[2]
|
|
|
|
if isReservedName(name) {
|
|
fmt.Printf("\033[1;31mError:\033[0m %s is a reserved name (constant, unit, function, or command)\n", name)
|
|
return false
|
|
}
|
|
|
|
res, target, err := evaluate(expr)
|
|
if err != nil {
|
|
fmt.Printf("\033[1;31mError:\033[0m %v\n", err)
|
|
return false
|
|
}
|
|
|
|
variables[name] = res
|
|
variables["ans"] = res
|
|
variables["_"] = res
|
|
if err := saveVariables(variablesFile); err != nil {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to persist variables: %v\n", err)
|
|
}
|
|
|
|
if target != "" {
|
|
fmt.Printf("\033[1;33m%s = %v %s\033[0m\n", name, res.Value, target)
|
|
} else if res.Dim == DimCurrency {
|
|
rateToBase := d(1.0).Div(eurRatesLookup(baseCurrency))
|
|
result := res.Value.Mul(rateToBase)
|
|
if result.IsInteger() {
|
|
fmt.Printf("\033[1;33m%s = %d %s\033[0m\n", name, result.IntPart(), baseCurrency)
|
|
} else {
|
|
f, _ := result.Float64()
|
|
fmt.Printf("\033[1;33m%s = %.4f %s\033[0m\n", name, f, baseCurrency)
|
|
}
|
|
} else if res.Dim != DimNone {
|
|
label := ""
|
|
for _, v := range unitRegistry {
|
|
if v.Dimension == res.Dim && v.Factor.Equal(d(1.0)) {
|
|
label = v.Label
|
|
break
|
|
}
|
|
}
|
|
fmt.Printf("\033[1;33m%s = %v %s\033[0m\n", name, res.Value, label)
|
|
} else {
|
|
if res.Value.IsInteger() {
|
|
fmt.Printf("\033[1;33m%s = %d\033[0m\n", name, res.Value.IntPart())
|
|
} else {
|
|
fmt.Printf("\033[1;33m%s = %v\033[0m\n", name, res.Value)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
res, target, err := evaluate(line)
|
|
if err != nil {
|
|
fmt.Printf("\033[1;31mError:\033[0m %v\n", err)
|
|
return false
|
|
}
|
|
|
|
variables["ans"] = res
|
|
variables["_"] = res
|
|
|
|
printResult(res, target)
|
|
return false
|
|
}
|
|
|
|
// printUpdateHint puts the note of the daily look on stderr, dimmed, so that it
|
|
// never lands in a pipe that only expects the result.
|
|
func printUpdateHint(hint string) {
|
|
if hint != "" {
|
|
fmt.Fprintf(os.Stderr, "\033[2m%s\033[0m\n", hint)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
// The options come before everything else: --update-refresh is the
|
|
// background run and must touch neither units nor rates, and --version has
|
|
// to answer at once — the updater probes the downloaded binary with it.
|
|
optY := false
|
|
newArgs := []string{os.Args[0]}
|
|
for i := 1; i < len(os.Args); i++ {
|
|
switch os.Args[i] {
|
|
case "-y":
|
|
optY = true
|
|
case "--version":
|
|
fmt.Printf("goca %s\n", Version)
|
|
return
|
|
case "--update":
|
|
if err := selfUpdate.install(os.Stdout); err != nil {
|
|
fmt.Fprintf(os.Stderr, "goca: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
case "--check-update":
|
|
if err := selfUpdate.check(os.Stdout); err != nil {
|
|
fmt.Fprintf(os.Stderr, "goca: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
case updateRefreshFlag: // the background run, not in the help
|
|
selfUpdate.refresh()
|
|
return
|
|
default:
|
|
newArgs = append(newArgs, os.Args[i])
|
|
}
|
|
}
|
|
os.Args = newArgs
|
|
|
|
initUnits()
|
|
|
|
// Costs nothing: the hint comes from the note in the cache, and the asking
|
|
// happens once a day at most, in the background. -y keeps it quiet.
|
|
updateHint := ""
|
|
if !optY {
|
|
updateHint = selfUpdate.daily()
|
|
}
|
|
|
|
hasArgs := len(os.Args) > 1
|
|
|
|
if err := fetchRates(); err != nil && !hasArgs {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to fetch rates: %v\n", err)
|
|
}
|
|
|
|
home, _ := os.UserHomeDir()
|
|
historyFile := filepath.Join(home, ".goca_history")
|
|
variablesFile := filepath.Join(home, ".goca_variables.json")
|
|
functionsFile := filepath.Join(home, ".goca_functions.json")
|
|
if err := loadUserFuncs(functionsFile); err != nil && !hasArgs {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to load persisted functions: %v\n", err)
|
|
}
|
|
|
|
if err := loadVariables(variablesFile); err != nil && !hasArgs {
|
|
fmt.Printf("\033[1;31mWarning:\033[0m Failed to load persisted variables: %v\n", err)
|
|
}
|
|
|
|
if hasArgs {
|
|
// Check for -p or --port flag
|
|
for i := 1; i < len(os.Args); i++ {
|
|
arg := os.Args[i]
|
|
if arg == "-p" || arg == "--port" {
|
|
if i+1 < len(os.Args) {
|
|
port := os.Args[i+1]
|
|
printUpdateHint(updateHint)
|
|
startWebServer(port)
|
|
return
|
|
} else {
|
|
fmt.Println("\033[1;31mError:\033[0m Missing port number after -p/--port")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
input := strings.Join(os.Args[1:], " ")
|
|
handleLine(input, false, variablesFile, functionsFile)
|
|
printUpdateHint(updateHint)
|
|
return
|
|
}
|
|
|
|
var useReadline = true
|
|
if fi, err := os.Stdin.Stat(); err == nil && (fi.Mode()&os.ModeCharDevice) == 0 {
|
|
useReadline = false
|
|
}
|
|
|
|
var rl *readline.Instance
|
|
if useReadline {
|
|
var err error
|
|
rl, err = readline.NewEx(&readline.Config{
|
|
Prompt: "\033[1;32mgoca>\033[0m ",
|
|
HistoryFile: historyFile,
|
|
InterruptPrompt: "^C",
|
|
EOFPrompt: "exit",
|
|
AutoComplete: &CalcCompleter{},
|
|
})
|
|
if err != nil {
|
|
useReadline = false
|
|
} else {
|
|
defer rl.Close()
|
|
}
|
|
}
|
|
|
|
if useReadline {
|
|
fmt.Printf("\033[1;34mgoca v%s\033[0m, type 'help' for examples.\n", Version)
|
|
printUpdateHint(updateHint)
|
|
}
|
|
|
|
var scanner *bufio.Scanner
|
|
if !useReadline {
|
|
scanner = bufio.NewScanner(os.Stdin)
|
|
}
|
|
|
|
for {
|
|
var line string
|
|
if useReadline {
|
|
var err error
|
|
line, err = rl.Readline()
|
|
if err != nil {
|
|
break
|
|
}
|
|
} else {
|
|
if !scanner.Scan() {
|
|
break
|
|
}
|
|
line = scanner.Text()
|
|
}
|
|
if handleLine(line, useReadline, variablesFile, functionsFile) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !useReadline { // piped input: the hint comes at the end, not before it
|
|
printUpdateHint(updateHint)
|
|
}
|
|
}
|