summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/repl/commands.go95
-rw-r--r--internal/repl/commands_test.go70
-rw-r--r--internal/repl/repl.go185
-rw-r--r--internal/version.go2
4 files changed, 351 insertions, 1 deletions
diff --git a/internal/repl/commands.go b/internal/repl/commands.go
new file mode 100644
index 0000000..7800653
--- /dev/null
+++ b/internal/repl/commands.go
@@ -0,0 +1,95 @@
+package repl
+
+import (
+ "fmt"
+ "strings"
+)
+
+// builtinCommands defines the built-in REPL commands
+var builtinCommands = []string{"help", "clear", "quit", "exit"}
+
+// Commands returns the list of built-in command names
+func Commands() []string {
+ return builtinCommands
+}
+
+// ExecuteCommand runs a built-in command and returns its output or error
+func ExecuteCommand(cmd string) (string, error) {
+ args := strings.Fields(cmd)
+ if len(args) == 0 {
+ return "", nil
+ }
+
+ switch strings.ToLower(args[0]) {
+ case "help":
+ return cmdHelp(args[1:]), nil
+ case "clear":
+ return "", cmdClear()
+ case "quit", "exit":
+ return "", cmdQuit()
+ default:
+ return "", fmt.Errorf("unknown command: %s. Available commands: help, clear, quit, exit", args[0])
+ }
+}
+
+func cmdHelp(subCmds []string) string {
+ helpText := `PERC - Percentage Calculator REPL
+
+Built-in Commands:
+ help Show this help message
+ help <command> Show help for a specific topic
+ clear Clear the screen
+ quit / exit Exit the REPL
+
+Usage Examples:
+ 20% of 150 Calculate 20% of 150
+ what is 20% of 150 Same as above (what is prefix is optional)
+ 30 is what % of 150 Calculate what percentage 30 is of 150
+ 30 is 20% of what Calculate what number 30 is 20% of
+
+Keyboard Shortcuts (Vi Mode):
+ Normal Mode:
+ i Enter Insert mode
+ a Append after cursor
+ 0 Move to beginning of line
+ $ Move to end of line
+ gg Move to top of history
+ G Move to bottom of history
+ Insert Mode:
+ ESC Return to Normal mode
+ Ctrl+C Cancel current input
+
+History Navigation:
+ Up Arrow Previous command
+ Down Arrow Next command
+
+Press Ctrl+D or type 'quit'/'exit' to exit.
+`
+
+ if len(subCmds) == 0 {
+ return helpText
+ }
+
+ subCmd := strings.ToLower(subCmds[0])
+ switch subCmd {
+ case "help":
+ return "help - Show this help message\nUsage: help [command]"
+ case "clear":
+ return "clear - Clear the screen\nUsage: clear"
+ case "quit", "exit":
+ return "quit / exit - Exit the REPL\nUsage: quit or exit"
+ default:
+ return fmt.Sprintf("No help available for: %s\nAvailable commands: help, clear, quit, exit", subCmd)
+ }
+}
+
+func cmdClear() error {
+ // Clear screen using ANSI escape sequence
+ fmt.Print("\033[2J\033[H")
+ return nil
+}
+
+func cmdQuit() error {
+ fmt.Println("Goodbye!")
+ return nil
+}
diff --git a/internal/repl/commands_test.go b/internal/repl/commands_test.go
new file mode 100644
index 0000000..7a37579
--- /dev/null
+++ b/internal/repl/commands_test.go
@@ -0,0 +1,70 @@
+package repl
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestExecuteCommandHelp(t *testing.T) {
+ output, err := ExecuteCommand("help")
+ if err != nil {
+ t.Fatalf("ExecuteCommand('help') returned error: %v", err)
+ }
+ if output == "" {
+ t.Error("ExecuteCommand('help') returned empty output")
+ }
+ if !strings.Contains(output, "PERC") {
+ t.Errorf("ExecuteCommand('help') output should contain 'PERC', got: %s", output[:50])
+ }
+}
+
+func TestExecuteCommandHelpWithSubcommand(t *testing.T) {
+ output, err := ExecuteCommand("help clear")
+ if err != nil {
+ t.Fatalf("ExecuteCommand('help clear') returned error: %v", err)
+ }
+ if output == "" {
+ t.Error("ExecuteCommand('help clear') returned empty output")
+ }
+ if !strings.Contains(output, "Clear") {
+ t.Errorf("ExecuteCommand('help clear') output should contain 'Clear', got: %s", output[:50])
+ }
+}
+
+func TestExecuteCommandUnknownCommand(t *testing.T) {
+ _, err := ExecuteCommand("unknown")
+ if err == nil {
+ t.Error("ExecuteCommand('unknown') should return error, got nil")
+ }
+ if !strings.Contains(err.Error(), "unknown command") {
+ t.Errorf("Error should mention 'unknown command', got: %v", err)
+ }
+}
+
+func TestExecuteCommandClear(t *testing.T) {
+ _, err := ExecuteCommand("clear")
+ if err != nil {
+ t.Fatalf("ExecuteCommand('clear') returned error: %v", err)
+ }
+}
+
+func TestExecuteCommandQuit(t *testing.T) {
+ _, err := ExecuteCommand("quit")
+ if err != nil {
+ t.Fatalf("ExecuteCommand('quit') returned error: %v", err)
+ }
+}
+
+func TestExecuteCommandExit(t *testing.T) {
+ _, err := ExecuteCommand("exit")
+ if err != nil {
+ t.Fatalf("ExecuteCommand('exit') returned error: %v", err)
+ }
+}
+
+func TestExecuteCommandEmpty(t *testing.T) {
+ _, err := ExecuteCommand("")
+ if err != nil {
+ t.Fatalf("ExecuteCommand('') returned error: %v", err)
+ }
+}
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]
+}
diff --git a/internal/version.go b/internal/version.go
index 8826dc9..f85f296 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -1,3 +1,3 @@
package internal
-const Version = "v0.1.0"
+const Version = "v0.2.0"