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: to/in (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 Delete variable (or 'unset *' to delete all)") fmt.Println(" base Change base currency (e.g., base USD)") fmt.Println(" clear Clear the terminal screen") fmt.Println(" exit/quit Exit goca") } 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 } func main() { initUnits() optY := false newArgs := []string{os.Args[0]} for i := 1; i < len(os.Args); i++ { if os.Args[i] == "-y" { optY = true } else { newArgs = append(newArgs, os.Args[i]) } } os.Args = newArgs if !optY { checkforupdate(UPDATEURL) } 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] 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) 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) } 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 } } }