From f93c53c5bacef868872e322834bd405ec3e433d4 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Sat, 24 May 2025 19:41:47 +0100
Subject: [PATCH] Add completer

---
 dclish/completer.go | 81 +++++++++++++++++++++++++++++++++++++++++++++
 dclish/dclish.go    |  5 +++
 repl/repl.go        |  8 +++--
 3 files changed, 91 insertions(+), 3 deletions(-)
 create mode 100644 dclish/completer.go

diff --git a/dclish/completer.go b/dclish/completer.go
new file mode 100644
index 0000000..82d7f67
--- /dev/null
+++ b/dclish/completer.go
@@ -0,0 +1,81 @@
+package dclish
+
+import (
+	"strings"
+	"unicode"
+)
+
+// Completer command completer type.
+type Completer struct {
+	commands []string
+	flags    map[string][]string
+}
+
+// NewCompleter creates a new completer.
+func NewCompleter(commands Commands) Completer {
+	comps := []string{}
+	flags := map[string][]string{}
+	for c := range commands {
+		comps = append(comps, c)
+		if len(commands[c].Commands) > 0 {
+			subcommands := commands[c].Commands
+			for subc := range subcommands {
+				fullc := c + " " + subc
+				comps = append(comps, fullc)
+				flags[fullc] = []string{}
+				if subcommands[subc].Flags != nil {
+					for f := range subcommands[subc].Flags {
+						flags[fullc] = append(flags[fullc], f)
+					}
+				}
+			}
+		}
+		flags[c] = []string{}
+		if commands[c].Flags != nil {
+			for f := range commands[c].Flags {
+				flags[c] = append(flags[c], f)
+			}
+		}
+	}
+	return Completer{
+		commands: comps,
+		flags:    flags,
+	}
+}
+
+// Do return a list of possible completions.
+func (c Completer) Do(line []rune, pos int) ([][]rune, int) {
+	// Nothing typed in.
+	if pos == 0 {
+		newline := make([][]rune, len(c.commands))
+		for i := range c.commands {
+			newline[i] = []rune(c.commands[i])
+		}
+		return newline, pos
+	}
+
+	// Command partially typed in.
+	newline := [][]rune{}
+	cmd := strings.ToUpper(string(line[0:pos]))
+	lower := false
+	if unicode.IsLower(line[0]) {
+		lower = true
+	}
+	for i := range c.commands {
+		if strings.HasPrefix(c.commands[i], cmd) {
+			rest := strings.Replace(c.commands[i], cmd, "", 1)
+			if lower {
+				newline = append(newline, []rune(strings.ToLower(rest)))
+			} else {
+				newline = append(newline, []rune(rest))
+			}
+		}
+	}
+	if len(newline) > 0 {
+		return newline, pos
+	}
+
+	// Command completely typed in.
+	// TODO: figure out flags.
+	return newline, pos
+}
diff --git a/dclish/dclish.go b/dclish/dclish.go
index 7736360..c0cb79b 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -10,6 +10,10 @@ import (
 // ActionFunc is the function that a command runs.
 type ActionFunc func(*Command) error
 
+// CompleterFunc is a function to provide completions for arguments for
+// a given command.
+type CompleterFunc func() []string
+
 // Flag is a flag for a command.
 type Flag struct {
 	OptArg      bool
@@ -30,6 +34,7 @@ type Command struct {
 	MinArgs     int
 	Commands    Commands
 	Action      ActionFunc
+	Completer   CompleterFunc
 	Description string
 }
 
diff --git a/repl/repl.go b/repl/repl.go
index 7dba020..1f69333 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	"unicode"
 
+	"git.lyda.ie/kevin/bulletin/dclish"
 	"git.lyda.ie/kevin/bulletin/this"
 	"github.com/adrg/xdg"
 	"github.com/chzyer/readline"
@@ -15,14 +16,15 @@ import (
 
 // Loop is the main event loop.
 func Loop() error {
+	completer := dclish.NewCompleter(commands)
 	histdir := path.Join(xdg.ConfigHome, "BULLETIN")
 	os.MkdirAll(histdir, 0700)
 	histfile := path.Join(histdir, fmt.Sprintf("%s.history", this.User.Login))
 	rl, err := readline.NewEx(
 		&readline.Config{
-			Prompt:      "BULLETIN> ",
-			HistoryFile: histfile,
-			// TODO: AutoComplete:    completer,
+			Prompt:            "BULLETIN> ",
+			HistoryFile:       histfile,
+			AutoComplete:      completer,
 			InterruptPrompt:   "^C",
 			EOFPrompt:         "EXIT",
 			HistorySearchFold: true,
-- 
GitLab