feat: Add help command and help definitions

Add support for the `/help` command, which dynamically builds a list of help commands based on the `help()` procs defined for each command type, ie `Login`.

The `help` proc returns a type of `HelpText`, which contains all the information necessary to render help information for that command. It contains the command, parameters (each parameters contains a name and description), aliases, and a description.

The help text is generated (at compile time) by a macro called `buildCommandHelp()`. Once the `/help` command is executed, the output of `buildCommandHelp()` is passed to the task runner so that a `HelpResult` event can be created and the help text can be displayed on screen. Because the text is generated at compile time, it is not necessary to pass this text through the task runner, and is displayed directly on screen in the main thread.

## NOTES
1. The help text is quite long, especially given there are blank spaces in between each command. This would require the chat client to be able to scroll unless it is sufficiently tall.
2. I am not married to the display output at all, even after experimenting with a few different layouts. There is some leftover rendering code in the `HelpResult` action which allows for the the command description to be written on the same line as the command name and keep the description spaced in line with all other descriptions, but it didn’t appear as readable as the format that I settled on in the end. I left it in there in case we wanted to change up how help is rendered.
This commit is contained in:
Eric Mastro 2021-07-01 19:28:19 +10:00
parent 3d0137e865
commit b6b1424116
No known key found for this signature in database
GPG Key ID: 141E3048D95A4E63
6 changed files with 178 additions and 19 deletions

View File

@ -48,6 +48,9 @@ proc connect*(self: ChatClient, username: string) {.async.} =
proc disconnect*(self: ChatClient) {.async.} =
asyncSpawn stopWakuChat2(self.taskRunner, status)
proc generateMultiAccount*(self: ChatClient, password: string) {.async.} =
asyncSpawn generateMultiAccount(self.taskRunner, status, password)
proc listAccounts*(self: ChatClient) {.async.} =
asyncSpawn listAccounts(self.taskRunner, status)
@ -57,8 +60,5 @@ proc login*(self: ChatClient, account: int, password: string) {.async.} =
proc logout*(self: ChatClient) {.async.} =
discard
proc generateMultiAccount*(self: ChatClient, password: string) {.async.} =
asyncSpawn generateMultiAccount(self.taskRunner, status, password)
proc sendMessage*(self: ChatClient, message: string) {.async.} =
asyncSpawn publishWakuChat2(self.taskRunner, status, message)

View File

@ -17,6 +17,8 @@ type
EventChannel* = AsyncChannel[ThreadSafeString]
# TODO: alphabetise ChatClient above HelpText -- didn't want to interfere
# with ongoing work
ChatClient* = ref object
account*: Account
chatConfig*: ChatConfig

View File

@ -1,5 +1,5 @@
import # std libs
std/strformat
std/[strformat, strutils]
import # chat libs
./parser

View File

@ -1,10 +1,10 @@
import # std libs
std/strutils
std/[sequtils, strformat, strutils, sugar]
import # chat libs
./screen, ./tasks
./common, ./macros, ./screen, ./tasks
export screen, strutils, tasks
export common, screen, strutils, tasks
logScope:
topics = "chat tui"
@ -27,6 +27,10 @@ logScope:
# Connect ----------------------------------------------------------------------
proc help*(T: type Connect): HelpText =
let command = "connect"
HelpText(command: command, description: "Connects to the waku network.")
proc new*(T: type Connect, args: varargs[string]): T {.raises: [].} =
T()
@ -42,6 +46,13 @@ proc command*(self: ChatTUI, command: Connect) {.async, gcsafe, nimcall.} =
# CreateAccount ----------------------------------------------------------------
proc help*(T: type CreateAccount): HelpText =
let command = "createaccount"
HelpText(command: command, aliases: aliased[command], parameters: @[
CommandParameter(name: "password", description: "Password for the new " &
"account.")
], description: "Creates a new Status account.")
proc new*(T: type CreateAccount, args: varargs[string]): T =
T(password: args[0])
@ -59,6 +70,10 @@ proc command*(self: ChatTUI, command: CreateAccount) {.async, gcsafe,
# Disconnect -------------------------------------------------------------------
proc help*(T: type Disconnect): HelpText =
let command = "disconnect"
HelpText(command: command, description: "Disconnects from the waku network.")
proc new*(T: type Disconnect, args: varargs[string]): T =
T()
@ -71,20 +86,14 @@ proc command*(self: ChatTUI, command: Disconnect) {.async, gcsafe, nimcall.} =
else:
self.wprintFormatError(epochTime().int64, "client is not online.")
# Help -------------------------------------------------------------------------
proc new*(T: type Help, args: varargs[string]): T =
T(command: args[0])
proc split*(T: type Help, argsRaw: string): seq[string] =
@[argsRaw.split(" ")[0]]
proc command*(self: ChatTUI, command: Help) {.async, gcsafe, nimcall.} =
let command = command.command
discard
# ListAccounts -----------------------------------------------------------------
proc help*(T: type ListAccounts): HelpText =
let command = "listaccounts"
HelpText(command: command, aliases: aliased[command], description: "Lists " &
"all existing Status accounts.")
proc new*(T: type ListAccounts, args: varargs[string]): T =
T()
@ -96,6 +105,14 @@ proc command*(self: ChatTUI, command: ListAccounts) {.async, gcsafe, nimcall.} =
# Login ------------------------------------------------------------------------
proc help*(T: type Login): HelpText =
let command = "login"
HelpText(command: command, parameters: @[
CommandParameter(name: "index", description: "Index of existing account, " &
"which can be retrieved using the `/list` command."),
CommandParameter(name: "password", description: "Account password.")
], description: "Logs in to the Status account.")
proc new*(T: type Login, args: varargs[string]): T {.raises: [].} =
T(account: args[0], password: args[1])
@ -130,6 +147,11 @@ proc command*(self: ChatTUI, command: Login) {.async, gcsafe, nimcall.} =
# Logout -----------------------------------------------------------------------
proc help*(T: type Logout): HelpText =
let command = "logout"
HelpText(command: command, description: "Logs out of the currently logged " &
"in Status account.")
proc new*(T: type Logout, args: varargs[string]): T =
T()
@ -141,6 +163,10 @@ proc command*(self: ChatTUI, command: Logout) {.async, gcsafe, nimcall.} =
# Quit -------------------------------------------------------------------------
proc help*(T: type Quit): HelpText =
let command = "quit"
HelpText(command: command, description: "Quits the chat client.")
proc new*(T: type Quit, args: varargs[string]): T =
T()
@ -152,6 +178,16 @@ proc command*(self: ChatTUI, command: Quit) {.async, gcsafe, nimcall.} =
# SendMessage ------------------------------------------------------------------
proc help*(T: type SendMessage): HelpText =
let command = ""
HelpText(command: command, aliases: aliased[command], parameters: @[
CommandParameter(name: "message", description: "Message to send on the " &
"network. Max length ???")
], description: "Sends a message on the waku network. By default, no " &
"command is needed. Text entered that is not preceded by a command will " &
"be interpreted as a send command, ie typing `hello` will be interpreted " &
"as `/send hello`.")
proc new*(T: type SendMessage, args: varargs[string]): T =
T(message: args[0])
@ -164,3 +200,99 @@ proc command*(self: ChatTUI, command: SendMessage) {.async, gcsafe, nimcall.} =
"client is not online, cannot send message.")
else:
asyncSpawn self.client.sendMessage(command.message)
# Help -------------------------------------------------------------------------
# Note: although "Help" is not alphabetically last, we need to keep this below
# all other `help()` definitions so that they are available to the
# `buildCommandHelp()` macro. Alternatively, we could use forward declarations
# at the top of the file, but this introduces an additional update for a
# developer implementing a new command.
proc help*(T: type Help): HelpText =
let command = "help"
HelpText(command: command, aliases: aliased[command], description:
"Show this help text.")
proc new*(T: type Help, args: varargs[string]): T =
T(command: args[0])
proc split*(T: type Help, argsRaw: string): seq[string] =
@[argsRaw.split(" ")[0]]
proc command*(self: ChatTUI, command: Help) {.async, gcsafe, nimcall.} =
let
command = command.command
helpTexts = buildCommandHelp()
timestamp = epochTime().int64
# display on screen
trace "TUI showing cli help", helpTexts=(%helpTexts)
proc forDisplay(help: HelpText): (string, string) =
let
command = help.command
hasAliases = help.aliases.len > 0
aliasPrefix = if hasAliases: ", /" else: ""
aliases = aliasPrefix & help.aliases.join(", /")
(command, aliases)
proc commandLength(help: HelpText): int =
let (command, aliases) = help.forDisplay
result = command.len + aliases.len
proc longest(helps: seq[HelpText]): int =
result = 0
for help in helps:
let length = help.commandLength
if length > result:
result = length
proc longest(params: seq[CommandParameter]): int =
result = 0
for param in params:
let length = param.name.len
if length > result:
result = length
if self.outputReady:
self.printResult("Available commands:", timestamp)
self.printResult("===================", timestamp)
self.printResult("", timestamp) # print blank line
let
spacing = 2
totalSpace = helpTexts.longest + spacing
for helpText in helpTexts:
let
(command, aliases) = helpText.forDisplay
desc = helpText.description
spaces = totalSpace - helpText.commandLength
paramsJoined = helpText.parameters.map(p => p.name).join("> <")
hasParams = helpText.parameters.len > 0
paramsList = if hasParams: fmt" <{paramsJoined}>" else: ""
finalText = fmt"{spacing.indent()}/{command}{aliases}{paramsList}"
self.printResult(finalText.replace("/, ", ""), timestamp)
self.printResult(fmt"{(spacing * 2).indent}{desc}", timestamp)
let
params = helpText.parameters
totalParamSpace = params.longest + spacing
if params.len > 0:
self.printResult(fmt"{(spacing * 2).indent}Parameters:", timestamp)
for param in params:
let
name = param.name
paramSpaces = totalParamSpace - name.len
desc = param.description
self.printResult(
fmt"{(spacing * 3).indent()}<{name}>{paramSpaces.indent}{desc}", timestamp)
# print a blank line
self.printResult("", timestamp)
trace "Sending help texts to the client", help=(%helpTexts)

View File

@ -43,6 +43,10 @@ type
Connect* = ref object of Command
CommandParameter* = ref object of RootObj
name*: string
description*: string
CreateAccount* = ref object of Command
password*: string
@ -51,6 +55,12 @@ type
Help* = ref object of Command
command*: string
HelpText* = ref object of RootObj
command*: string
parameters*: seq[CommandParameter]
aliases*: seq[string]
description*: string
ListAccounts* = ref object of Command
Login* = ref object of Command

View File

@ -2,7 +2,7 @@ import # std libs
std/macros
import # chat libs
./common
./common, ../client/common as client_common
export common
@ -103,3 +103,18 @@ macro commandSplitCases*(): untyped =
casenode.add(ofbranch)
result.add(casenode)
macro buildCommandHelp*(): untyped =
result = newStmtList()
let helpId = ident("_help_")
result.add quote do:
var `helpId`: seq[HelpText] = @[]
for command in commands:
let
commandType = ident(command)
result.add(quote do: `helpId`.add(`commandType`.help()))
result.add quote do:
`helpId`