From f029cbfbb4d89d55aee01fc89f08b0388c45575e Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Tue, 6 May 2025 21:55:41 +0100
Subject: [PATCH] Remove NEWS related things

Remove NEWS related commands, flags and help text.
---
 accounts/accounts.go |  10 +-
 dclish/dclish.go     |  17 +++-
 go.mod               |   1 +
 go.sum               |   2 +
 repl/command.go      | 236 +++++++++----------------------------------
 repl/folders.go      |  29 ++++++
 repl/help.go         |  60 ++++++++++-
 7 files changed, 159 insertions(+), 196 deletions(-)
 create mode 100644 repl/folders.go

diff --git a/accounts/accounts.go b/accounts/accounts.go
index acd5b25..def5a4b 100644
--- a/accounts/accounts.go
+++ b/accounts/accounts.go
@@ -16,10 +16,12 @@ import (
 // UserData is the type for holding user data. Things like preferences,
 // unread message counts, signatures, etc.
 type UserData struct {
-	Account  string
-	FullName string
-	pref     *sql.DB
-	bull     *sql.DB
+	Account        string
+	FullName       string
+	pref           *sql.DB
+	bull           *sql.DB
+	CurrentFolder  string
+	CurrentMessage int
 }
 
 // User is the user for this process.  It is loaded by the `Verify` function.
diff --git a/dclish/dclish.go b/dclish/dclish.go
index 94ebb59..19c93fe 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -54,7 +54,8 @@ func (c Commands) ParseAndRun(line string) error {
 		case 1:
 			cmd = c[possibles[0]]
 		default:
-			fmt.Printf("ERROR: Ambiguous command '%s' (matches %s)\n", words[0], possibles)
+			fmt.Printf("ERROR: Ambiguous command '%s' (matches %s)\n",
+				words[0], strings.Join(possibles, ", "))
 			return nil
 		}
 	}
@@ -78,7 +79,8 @@ func (c Commands) ParseAndRun(line string) error {
 				wordup := strings.ToUpper(flag)
 				flg, ok := cmd.Flags[wordup]
 				if !ok {
-					fmt.Printf("ERROR: Flag '%s' not recognised.", args[i])
+					fmt.Printf("ERROR: Flag '%s' not recognised.\n", args[i])
+					return nil
 				}
 				flg.Value = val
 			} else {
@@ -90,13 +92,22 @@ func (c Commands) ParseAndRun(line string) error {
 				}
 				flg, ok := cmd.Flags[wordup]
 				if !ok {
-					fmt.Printf("ERROR: Flag '%s' not recognised.", args[i])
+					fmt.Printf("ERROR: Flag '%s' not recognised.\n", args[i])
+					return nil
 				}
 				flg.Value = value
 			}
 		} else {
+			if len(cmd.Args) == cmd.MaxArgs {
+				fmt.Printf("ERROR: Too many args at '%s'\n", args[i])
+				return nil
+			}
 			cmd.Args = append(cmd.Args, args[i])
 		}
 	}
+	if len(cmd.Args) < cmd.MinArgs {
+		fmt.Println("ERROR: Not enough args.")
+		return nil
+	}
 	return cmd.Action(cmd)
 }
diff --git a/go.mod b/go.mod
index f7faa44..cba97c6 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
diff --git a/go.sum b/go.sum
index cd2ef07..a9657c7 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
 github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
diff --git a/repl/command.go b/repl/command.go
index ad080b4..a963b64 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -163,9 +163,7 @@ useful for scanning a long message.`,
  
 Specifies that if a message header exists, the header will be shown.
 If /HEADER or /NOHEADER is specified, the setting will apply for all
-further reads in the selected folder.  The default is /HEADER for non-
-NEWS folders, /NOHEADER for NEWS folders.  If the SET STRIP command
-is set for the folder, it will change the default to be /HEADER.`,
+further reads in the selected folder.  The default is /HEADER.`,
 			},
 		},
 	},
@@ -275,12 +273,6 @@ in place of an actual number, i.e. CURRENT-LAST, 1-CURRENT, etc.`,
 			"/ALL": {
 				Description: `Specifies to copy all the messages in the old folder.`,
 			},
-			"/GROUPS": {
-				Description: `/GROUPS=(newsgroup,[...])
- 
-Valid only if a NEWS group is selected.  Specifies to send the message to
-the specified NEWS group(s) in addition to the selected NEWS group.`,
-			},
 			"/HEADER": {
 				Description: `/[NO]HEADER
  
@@ -464,9 +456,7 @@ useful for scanning a long message.`,
  
 Specifies that if a message header exists, the header will be shown.
 If /HEADER or /NOHEADER is specified, the setting will apply for all
-further reads in the selected folder.  The default is /HEADER for non-
-NEWS folders, /NOHEADER for NEWS folders.  If the SET STRIP command
-is set for the folder, it will change the default to be /HEADER.`,
+further reads in the selected folder.  The default is /HEADER.`,
 			},
 		},
 	},
@@ -544,6 +534,7 @@ newest message.  If there are no new messages, listing will start at the
 first message, or if a message has already been read, it will  start  at
 that message.`,
 		MaxArgs: 1,
+		Action:  ActionDirectory,
 		Flags: dclish.Flags{
 			"/ALL": {
 				Description: `Lists all messages.  Used if the qualifiers /MARKED, /UNMARKED, /SEEN,
@@ -564,8 +555,7 @@ Indicates the last message number you want to display.`,
 			"/FOLDERS": {
 				Description: `Lists the available message folders.  Shows last message date and number
 of  messages  in  folder.   An asterisk (*) next to foldername indicates
-that there are unread messages in  that  folder.   This  will  not  show
-newsgroups.  To see newsgroups, use the NEWS command or DIR/NEWS.`,
+that there are unread messages in  that  folder.`,
 			},
 			"/MARKED": {
 				Description: `Lists messages that have been marked (indicated by an asterisk).
@@ -595,10 +585,6 @@ read.  To see all messages, use either /ALL, or reselect the folder.`,
 			"/NEW": {
 				Description: `Specifies  to  start  the  listing  of  messages  with  the first unread
 message.`,
-			},
-			"/NEWS": {
-				Description: `Lists the available news groups.  This does the same thing as  the  NEWS
-command.  See that command for qualifiers which apply.`,
 			},
 			"/PRINT": {
 				Description: `Specifies that the text of the messages which are found by the
@@ -692,7 +678,8 @@ file exists, the file would be appended to that file.`,
 		Description: `To obtain help on any topic, type:
  
         HELP  topic`,
-		Action: ActionHelp,
+		MaxArgs: 1,
+		Action:  ActionHelp,
 	},
 	"INDEX": {
 		Description: `Gives directory listing of all folders in alphabetical order. If the
@@ -703,6 +690,7 @@ off after one has read a message.
  
   Format:
        INDEX`,
+		Action: ActionIndex,
 		Flags: dclish.Flags{
 			"/MARKED": {
 				Description: `Lists messages that have been marked (marked messages are indicated by
@@ -779,9 +767,7 @@ useful for scanning a long message.`,
  
 Specifies that if a message header exists, the header will be shown.
 If /HEADER or /NOHEADER is specified, the setting will apply for all
-further reads in the selected folder.  The default is /HEADER for non-
-NEWS folders, /NOHEADER for NEWS folders.  If the SET STRIP command
-is set for the folder, it will change the default to be /HEADER.`,
+further reads in the selected folder.  The default is /HEADER.`,
 			},
 		},
 	},
@@ -834,11 +820,27 @@ as unmarked.
        MARK [message-number or numbers]
        UNMARK [message-number or numbers]
  
-NOTE: The list of marked messages for non-NEWS folders are stored in a
-file username.BULLMARK, and NEWS folders are stored in
-username.NEWSMARK. The files are created in the directory pointed to by
-the logical name BULL_MARK.  If BULL_MARK is not defined, SYS$LOGIN
-will be used.`,
+NOTE: The list of marked messages are stored in a file username.BULLMARK.
+The files are created in the directory pointed to by the logical name
+BULL_MARK.  If BULL_MARK is not defined, SYS$LOGIN will be used.`,
+		MaxArgs: 1,
+	},
+	"UNMARK": {
+		Description: `Sets the current or message-id message as marked. Marked messages are
+displayed with an asterisk in the left hand column of the directory
+listing.  A marked message can serve as a reminder of important
+information.  The UNMARK command sets the current or message-id message
+as unmarked.
+ 
+   Format:
+ 
+       MARK [message-number or numbers]
+       UNMARK [message-number or numbers]
+ 
+NOTE: The list of marked messages are stored in a file username.BULLMARK.
+The files are created in the directory pointed to by the logical name
+BULL_MARK.  If BULL_MARK is not defined, SYS$LOGIN will be used.`,
+		MaxArgs: 1,
 	},
 	"MODIFY": {
 		Description: `Modifies the database information for the current folder.  Only the
@@ -906,19 +908,6 @@ in place of an actual number, i.e. CURRENT-LAST, 1-CURRENT, etc.`,
 				Description: `Specifies to move all the messages from the old folder.  Note:  If the
 old folder is remote, they will be copied but not deleted, as only one
 message can be deleted from a remote folder at a time.`,
-			},
-			"/GROUPS": {
-				Description: `/GROUPS=(newsgroup,[...])
- 
-Valid only if a NEWS group is selected.  Specifies to send the message to
-the specified NEWS group(s) in addition to the selected NEWS group.`,
-			},
-			"/HEADER": {
-				Description: `/[NO]HEADER
- 
-Valid only if destination folder is a news group.  Specifies that header
-of message is to be included with the text when the text is copied.
-The default is /NOHEADER.`,
 			},
 			"/MERGE": {
 				Description: `Specifies that the original date and time of the moved messages are
@@ -946,82 +935,7 @@ useful for scanning a long message.`,
  
 Specifies that if a message header exists, the header will be shown.
 If /HEADER or /NOHEADER is specified, the setting will apply for all
-further reads in the selected folder.  The default is /HEADER for non-
-NEWS folders, /NOHEADER for NEWS folders.  If the SET STRIP command
-is set for the folder, it will change the default to be /HEADER.`,
-			},
-		},
-	},
-	"POST": {
-		Description: `If a NEWS group is selected, posts a message to that group.  If a normal
-folder is selected, sends a message via MAIL to the network mailing list
-which  is  associated  with  the  selected  folder.   The address of the
-mailing  list  must  be  stored  using  either   CREATE/DESCRIPTION   or
-MODIFY/DESCRIPTION.  See help on those commands for more information.
- 
-  Format:
-    POST [file-name]`,
-		MaxArgs: 1,
-		Flags: dclish.Flags{
-			"/CC": {
-				Description: `/CC=user[s]
-Specifies additional users that should receive the mail message.`,
-			},
-			"/EDIT": {
-				Description: `Specifies that the editor is to be used for creating the mail message.`,
-			},
-			"/EXTRACT": {
-				Description: `Specifies that the text of the message that is being read should be
-included in the mail message.  This qualifier is valid only when used
-with /EDIT.  The text of the message is indented with > at the
-beginning of each line.  This can be suppressed with /NOINDENT.`,
-			},
-			"/GROUPS": {
-				Description: `/GROUPS=(newsgroup,[...])
- 
-Valid only if a NEWS group is selected.  Specifies to send the message to
-the specified NEWS group(s) in addition to the selected NEWS group.`,
-			},
-			"/NOINDENT": {
-				Description: `See /EXTRACT for information on this qualifier.`,
-			},
-			"/NOSIGNATURE": {
-				Description: `Specifies to suppress the automatically appended signature, if one exists.
-Signatures are appended for postings to mailing lists and to responds.
-See the help topic POST Signature_file for signature information.`,
-			},
-			"/SUBJECT": {
-				Description: `/SUBJECT=text
- 
-Specifies the subject of the mail message. If the text consists of more
-than one word, enclose the text in quotation marks (").
- 
-If you omit this qualifier, you will prompted for the subject.`,
-			},
-			"Signature_file": {
-				Description: `It is possibly to have the contents of a file be automatically appended
-to the end of a message added with the POST and/or the RESPOND command.
-This file is known as a signature file, and it typically contains one's
-name, address, or perhaps a favorite quote.  The name of the file should
-be SYS$LOGIN:BULL_SIGNATURE.TXT, and it should be a simple text file.  In
-order to specify a different file to use, define the logical name
-BULL_SIGNATURE to point to the desired file.
- 
-It is possible to specify that portions or all of the signature file are
-to be included only for specific folders or news groups.  Simply surround
-the exclusive text starting with the line "START <folder-name>" and ending
-with the line "END", i.e.
- 
-START INFOVAX
-This line will only appear in the INFOVAX folder.
-END
-START MISC.TEST
-This line will only appear in the news folder MISC.TEST.
-END
-This line will appear in all postings.
- 
-Note that an empty line is automatically created to separate the text of
-the message and the contents of the signature file.`,
+further reads in the selected folder.  The default is /HEADER.`,
 			},
 		},
 	},
@@ -1127,9 +1041,7 @@ useful for scanning a long message.`,
  
 Specifies that if a message header exists, the header will be shown.
 If /HEADER or /NOHEADER is specified, the setting will apply for all
-further reads in the selected folder.  The default is /HEADER for non-
-NEWS folders, /NOHEADER for NEWS folders.  If the SET STRIP command
-is set for the folder, it will change the default to be /HEADER.`,
+further reads in the selected folder.  The default is /HEADER.`,
 			},
 			"/MARKED": {
 				Description: `Specifies to read only messages that have been marked (marked messages
@@ -1233,13 +1145,6 @@ message.`,
 mail message.  This qualifier is valid only when used with /EDIT.  The
 text of the message is indented with > at the beginning of each line.
 This can be suppressed with /NOINDENT.`,
-			},
-			"/GROUPS": {
-				Description: `/GROUPS=(newsgroup,[...])
- 
-Valid only if a NEWS group is selected and /LIST is present.  Specifies
-to send the message to the specified NEWS group(s) in addition to the
-selected NEWS group.`,
 			},
 			"/LIST": {
 				Description: `Specifies that the reply should also be sent to the network mailing list
@@ -1333,18 +1238,14 @@ command sets the current or message-id message as unseen.
        SEEN [message-number or numbers]
        UNSEEN [message-number or numbers]
  
-Keeping track of seen messages requires very little overhead for NEWS
-folders.  However, there is a moderate overhead for regular non-NEWS
-folders.  If you have used the SEEN command and wish to disable the
-automatic marking of messages in regular folders as SEEN when they are
-read, type the command SEEN/NOREAD.  To reenable, simply use the SEEN
-command again. 
- 
-NOTE: The list of SEEN messages for non-NEWS folders are stored in a
-file username.BULLMARK, and NEWS folders are stored in
-username.NEWSMARK. The files are created in the directory pointed to by
-the logical name BULL_MARK.  If BULL_MARK is not defined, SYS$LOGIN
-will be used.`,
+If you have used the SEEN command and wish to disable the automatic
+marking of messages in regular folders as SEEN when they are read, type
+the command SEEN/NOREAD.  To reenable, simply use the SEEN command again.
+ 
+NOTE: The list of SEEN messages are stored in a
+file username.BULLMARK.  NEWSMARK. The files are created in the directory
+pointed to by the logical name BULL_MARK.  If BULL_MARK is not defined,
+SYS$LOGIN will be used.`,
 		MinArgs: 1,
 		MaxArgs: 1,
 	},
@@ -1361,18 +1262,14 @@ command sets the current or message-id message as unseen.
        SEEN [message-number or numbers]
        UNSEEN [message-number or numbers]
  
-Keeping track of seen messages requires very little overhead for NEWS
-folders.  However, there is a moderate overhead for regular non-NEWS
-folders.  If you have used the SEEN command and wish to disable the
-automatic marking of messages in regular folders as SEEN when they are
-read, type the command SEEN/NOREAD.  To reenable, simply use the SEEN
-command again. 
- 
-NOTE: The list of SEEN messages for non-NEWS folders are stored in a
-file username.BULLMARK, and NEWS folders are stored in
-username.NEWSMARK. The files are created in the directory pointed to by
-the logical name BULL_MARK.  If BULL_MARK is not defined, SYS$LOGIN
-will be used.`,
+If you have used the SEEN command and wish to disable the automatic
+marking of messages in regular folders as SEEN when they are read, type
+the command SEEN/NOREAD.  To reenable, simply use the SEEN command again.
+ 
+NOTE: The list of SEEN messages are stored in a
+file username.BULLMARK.  NEWSMARK. The files are created in the directory
+pointed to by the logical name BULL_MARK.  If BULL_MARK is not defined,
+SYS$LOGIN will be used.`,
 		MinArgs: 1,
 		MaxArgs: 1,
 	},
@@ -1397,14 +1294,7 @@ BULLCP process running (invoked by BULLETIN/STARTUP command.)
  
 After selecting a folder, the user will notified of the number of unread
 messages,  and  the  message  pointer will be placed at the first unread
-message.
- 
-BULLETIN automatically determines if the selcted name is a NEWS group by
-detecting if a period is present in the name being  specified,  as  most
-NEWS  groups  contain  a  period,  whereas  a real folder cannot.  A few
-special NEWS groups, i.e. JUNK and CONTROL, do not contain a period.  If
-desired,  you can select these groups by enclosing them in double quotes
-("), and typing the name in lower case.`,
+message.`,
 		Flags: dclish.Flags{
 			"/MARKED": {
 				Description: `Selects  only messages that have been marked (indicated by an asterisk).
@@ -1524,8 +1414,8 @@ users must be at least 2.
  
     SET BBOARD [username]
  
-BBOARD cannot be set for remote folders.   See  also  the  commands  SET
-STRIP and SET DIGEST for options on formatting BBOARD messages.
+BBOARD cannot be set for remote folders.   See  also  the  command  SET
+DIGEST for options on formatting BBOARD messages.
  
 If BULLCP is running, BBOARD is updated every 15 minutes.  If you want
 to length this period, define BULL_BBOARD_UPDATE to be the number of
@@ -1980,19 +1870,6 @@ specified, the selected folder is modified. Valid only with NOSHOWNEW.
  
 Specifies that SHOWNEW is a permanent flag and cannot be changed by the
 individual, except if changing to READNEW. This is a privileged qualifier.
-`,
-			},
-			"STRIP": {
-				Description: `Affect only messages which are added via either the BBOARD option, or
-written directly from a network mailing program (i.e. PMDF).  If
-STRIP is set, the header of the mail message will be stripped off
-before it is stored as a BULLETIN message.
- 
-  Format:
- 
-    SET [NO]STRIP
- 
-The command SHOW FOLDER/FULL will show if STRIP has been set.
 `,
 			},
 			"SYSTEM": {
@@ -2118,13 +1995,6 @@ linked.
 			},
 		},
 	},
-	"SUBSCRIBE": {
-		Description: `Used only for NEWS folders.  Specifies that BULLETIN will keep track  of
-the  newest  message  that has been read for that NEWS folder.  When the
-NEWS folder is selected, the message pointer will automatically point to
-the next newest message that has not been read.
-`,
-	},
 	"UNDELETE": {
 		Description: `Undeletes  he  specified  message  if  the message was deleted using the
 DELETE command.  Deleted messages are  not  actually  deleted  but  have
@@ -2135,12 +2005,6 @@ string (DELETED) when either reading or doing a directory listing.
  
   Format:
     UNDELETE [message-number]
-`,
-	},
-	"UNSUBSCRIBE": {
-		Description: `Used only for NEWS folders.  Specifies that BULLETIN will no longer keep
-track of the newest message that has been read for that NEWS folder.  See the
-SUBSCRIBE command for further info. 
 `,
 	},
 }
diff --git a/repl/folders.go b/repl/folders.go
new file mode 100644
index 0000000..3e0db73
--- /dev/null
+++ b/repl/folders.go
@@ -0,0 +1,29 @@
+// Package repl implements the main event loop.
+package repl
+
+import (
+	"fmt"
+	"strings"
+
+	"git.lyda.ie/kevin/bulletin/accounts"
+	"git.lyda.ie/kevin/bulletin/dclish"
+)
+
+// ActionDirectory handles the `DIRECTORY` command.  This lists all the
+// messages in the current folder.
+func ActionDirectory(cmd *dclish.Command) error {
+	// TODO: flag parsing.
+	if len(cmd.Args) == 1 {
+		folder := strings.ToUpper(cmd.Args[0])
+		// TODO: Check folder is valid.
+		accounts.User.CurrentFolder = folder
+	}
+	fmt.Println("TODO: List messages in folder")
+	return nil
+}
+
+// ActionIndex handles the `INDEX` command.  This lists all the folders.
+func ActionIndex(cmd *dclish.Command) error {
+	fmt.Printf("TODO: implement INDEX:\n%s\n\n", cmd.Description)
+	return nil
+}
diff --git a/repl/help.go b/repl/help.go
index 007dfe0..d3dc916 100644
--- a/repl/help.go
+++ b/repl/help.go
@@ -3,6 +3,7 @@ package repl
 
 import (
 	"fmt"
+	"sort"
 	"strings"
 
 	"git.lyda.ie/kevin/bulletin/dclish"
@@ -64,9 +65,61 @@ in a state such that they would be inaccessible by other users.)
 }
 
 func init() {
+	// Add all command help.
+	buf := &strings.Builder{}
 	for c := range commands {
-		helpmap[c] = commands[c].Description
+		fmt.Fprint(buf, commands[c].Description)
+		if len(commands[c].Flags) > 0 {
+			flgs := make([]string, len(commands[c].Flags))
+			i := 0
+			for flg := range commands[c].Flags {
+				flgs[i] = flg
+				i++
+			}
+			sort.Strings(flgs)
+			for i := range flgs {
+				fmt.Fprintf(buf, "\n%s %s", flgs[i], commands[c].Flags[flgs[i]].Description)
+			}
+		}
+		helpmap[c] = buf.String()
+		buf.Reset()
+	}
+
+	// Add a list of topics.
+	topics := make([]string, len(helpmap))
+	i := 0
+	maxlen := 0
+	for topic := range helpmap {
+		maxlen = max(len(topic), maxlen)
+		topics[i] = topic
+		i++
+	}
+	maxlen = maxlen + 2
+	sort.Strings(topics)
+
+	buf.Reset()
+	linelen := 2
+	fmt.Fprint(buf, `The following command and topics are available for more help
+
+  `)
+	for i := range topics {
+		linelen += maxlen
+		if linelen > 78 {
+			fmt.Fprint(buf, "\n  ")
+			linelen = maxlen + 2
+		}
+		// TODO: Kludge - move helpmap to map[string]TopicType which has a
+		// description and a display name.
+		switch topics[i] {
+		case "FOLDERS":
+			fmt.Fprintf(buf, "%-*s", maxlen, "Folders")
+		case "CTRL-C":
+			fmt.Fprintf(buf, "%-*s", maxlen, "Ctrl-C")
+		default:
+			fmt.Fprintf(buf, "%-*s", maxlen, topics[i])
+		}
 	}
+	helpmap["TOPICS"] = buf.String()
 }
 
 // ActionHelp handles the `HELP` command.
@@ -91,11 +144,12 @@ func ActionHelp(cmd *dclish.Command) error {
 		case 1:
 			helptext = helpmap[possibles[0]]
 		default:
-			fmt.Printf("ERROR: Ambiguous topic '%s' (matches %s)\n", cmd.Args[0], possibles)
+			fmt.Printf("ERROR: Ambiguous topic '%s' (matches %s)\n",
+				cmd.Args[0], strings.Join(possibles, ", "))
 			return nil
 		}
 
 	}
-	fmt.Printf("%s\n\n", helptext)
+	fmt.Printf("\n%s\n\n", helptext)
 	return nil
 }
-- 
GitLab