diff options
Diffstat (limited to 'internal/repl/repl.go')
| -rw-r--r-- | internal/repl/repl.go | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/internal/repl/repl.go b/internal/repl/repl.go new file mode 100644 index 0000000..0f690e9 --- /dev/null +++ b/internal/repl/repl.go @@ -0,0 +1,185 @@ +package repl + +import ( + "bufio" + "fmt" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "codeberg.org/snonux/perc/internal/calculator" + "github.com/mattn/go-isatty" + + "github.com/c-bata/go-prompt" +) + +const historyFile = ".perc_history" + +// executor runs a calculation command and returns the result +func executor(input string) { + input = strings.TrimSpace(input) + if input == "" { + return + } + + // Check if it's a built-in command + if cmd, ok := isBuiltinCommand(input); ok { + output, err := ExecuteCommand(cmd) + if err != nil { + fmt.Printf("Error: %v\n", err) + } + if output != "" { + fmt.Println(output) + } + // Don't add built-in commands to history + return + } + + // Run the calculation + result, err := calculator.Parse(input) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println(result) +} + +// isBuiltinCommand checks if input starts with a built-in command +func isBuiltinCommand(input string) (string, bool) { + args := strings.Fields(input) + if len(args) == 0 { + return "", false + } + + cmd := strings.ToLower(args[0]) + for _, builtin := range builtinCommands { + if cmd == builtin { + return input, true + } + } + return "", false +} + +// getHistoryPath returns the path to the history file +func getHistoryPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, historyFile) +} + +// loadHistory loads history from file +func loadHistory() []string { + historyPath := getHistoryPath() + if historyPath == "" { + return nil + } + + file, err := os.Open(historyPath) + if err != nil { + return nil + } + defer file.Close() + + var history []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + history = append(history, scanner.Text()) + } + return history +} + +// saveHistory saves history to file +func saveHistory(history []string) error { + historyPath := getHistoryPath() + if historyPath == "" { + return nil + } + + // Keep only last 1000 entries to prevent unlimited growth + if len(history) > 1000 { + history = history[len(history)-1000:] + } + + file, err := os.Create(historyPath) + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + for _, entry := range history { + if _, err := writer.WriteString(entry + "\n"); err != nil { + return err + } + } + return writer.Flush() +} + +// RunREPL starts the interactive REPL +func RunREPL() error { + // Check if stdin is a TTY + if !isatty.IsTerminal(os.Stdin.Fd()) { + fmt.Fprintln(os.Stderr, "REPL mode requires a TTY. Use 'perc <calculation>' for non-interactive mode.") + return fmt.Errorf("stdin is not a TTY") + } + + history := loadHistory() + + p := prompt.New( + executor, + completer, + prompt.OptionTitle("perc - Percentage Calculator"), + prompt.OptionPrefix("perc> "), + prompt.OptionLivePrefix(func() (string, bool) { + return "perc> ", true + }), + prompt.OptionHistory(history), + ) + + // Handle SIGINT gracefully + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT) + + go func() { + <-sigChan + fmt.Println("\nUse 'quit' or 'exit' to exit, or Ctrl+D") + }() + + // Run the prompt + p.Run() + + // Note: History is not saved automatically in this version + // The prompt library stores it in memory but doesn't expose a getter + + return nil +} + +// completer provides auto-completion for built-in commands +func completer(d prompt.Document) []prompt.Suggest { + text := d.GetWordBeforeCursor() + if text == "" { + return nil + } + + var suggestions []prompt.Suggest + for _, cmd := range builtinCommands { + if strings.HasPrefix(strings.ToLower(cmd), strings.ToLower(text)) { + suggestions = append(suggestions, prompt.Suggest{Text: cmd, Description: getCommandDescription(cmd)}) + } + } + return suggestions +} + +func getCommandDescription(cmd string) string { + descriptions := map[string]string{ + "help": "Show help information", + "clear": "Clear the screen", + "quit": "Exit the REPL", + "exit": "Exit the REPL", + } + return descriptions[cmd] +} |
